java.io包的困惑
对于初识java的程序员来说,甚至已经工作三五年的java老鸟们,对java.io包中各种“流”以及五花八门的api都是浑浑噩噩搞不清(笔者在刚接触java时也经历过同样的迷茫)。但如果你已经熟悉了“装饰器模式”的话,再来看一遍java.io中API,就会有一种豁然开朗的感觉。
继承是实现类复用的重要手段,但却不是唯一的手段,通过类的关联组合同样可以做到,而且如果使用得当 比通过继承更富有弹性。“装饰器模式”就是通过组合来实现类似复用和包装,这就是OO设计的另一个原则“合成复用原则”:则是尽量使用合成/聚合的方式,而不是使用继承。
在讲解java.io包之前,先来熟悉下“装饰器模式”。
装饰器模式
装饰器模式可以动态的把新的职责添加到对象上,在扩展性方面比通过继承实现扩展更富有弹性。这里关键点是“动态”,也就是运行时;而继承在编译的时候已经确定了。先来看下类图:
从类图中可以看到该模式有4类角色:
其中 Component是抽象的接口;ConcreteComponent0、ConcreteComponent1是具体的实现 也就是具体被装饰者;Decorator是抽象的装饰者可以是接口或抽象类,它跟“被装饰者”处在同一层级上;ConcreteDecoratorA、ConcreteDecoratorB、ConcreteDecoratorC具体的装饰者在同 他们的主要作用就是对“具体的被装饰者”进行包装,附件新的功能。
该模式的核心就是,抽象的装饰者Decorator,它起到承上启下的作用:继承(或实现) Component,使得所有的“装饰器”和“被装饰器”类型相同(实现针对接口编程);同时Decorator中定义了一个Component类型的成员变量,指向“具体的被装饰者”,通过“组合”的方式实现对其进行包装。
也许你会说这里Decorator是继承自Component,也用到了“继承”。但这里继承的作用仅仅是为了跟“具体的被装饰者”类型保持相同,主要还是通过组合的方式(即成员变量)对component成员进行包装。
“具体的装饰器”不仅可以包装“具体的被装饰者”,它还可以多层嵌套包装“具体的装饰器”(最内层必须是“具体的被装饰者”),从而实现动态的多个附加功能的添加。
示例展示
本示例场景是做web开发中都会遇到的页面渲染场景:业务需求是要渲染PC页面(电脑版页面)、M页面(移动端页面);同时针对不同的页面需要 缓存到redis、缓存cdn、抓取页面上的sku。这时就产生了一系列的组合,PC页面+reddis缓存+抓取sku、PC页面+redis缓存、M页面、M页面+redis缓存 等等一系列的组合。解决办法有三种:
方式一:继承,如果为每个类型创建新的子类,势必会产生一些列的子类,而且毫无复用性可言。直接放弃。
方式二:继承,当然也许你会想到另外一种解决方式:只创建 PC页面、M页面渲染类,把 reddis缓存、cdn缓存、sku抓取作为单独的方法,再进行一些列的判断 来实现复用:
这里以及PCRenderServcie实现为例:
public class PCRenderServcie extends RenderService{ private boolean isCdn; private boolean isRedis; private boolean isGetSku; public void render(){ System.out.println("完成pc基础页面渲染"); if(isCdn){ cdnHandle(); System.out.println("cdn缓存处理"); } if(isRedis){ redisHandle(); System.out.println("redis缓存处理"); } if(isGetSku){ skuHandle(); System.out.println("抓取页面上的sku"); } } //省略getter setter方法 }
这里把RenderService设计成抽象类,并把这些方法cdnHandle()、redisHandle()、skuHandle()公共方法提取到RenderService中,MRenderServcie的实现与上述PCRenderServcie类似 可以共用这些方法。
这种方式从一定程度上 实现了部分代码复用,并且解决方式一中的“子类泛滥”的问题。但缺点也很明显,假设现在要新增一个操作,所有RenderService、PCRenderServcie、MRenderServcie都会涉及到修改。无法满足“开闭原则”,对以前的代码造成破坏。
前面两种方式都是尝试使用“继承”来解决问题,我们可以看到效果都不是很理想。下面来看看今天的主角“装饰器模式”,是如何解决上述问题的。
方式三:装饰器模式,采用装饰器模式,我们首先要区分出业务场景中 那些是“被装饰者”,哪些是“装饰者”。从方式二中 其实就可以区分开来:PCRenderServcie和MRenderServcie是基础操作 可以被看做是“被装饰者”;是否cdn缓存、是否redis缓存、是否抓取sku,这些动作可以看做是“可有可无”的“装饰者”,用来增强“被装饰者”的基础操作。
区分出“被装饰者”和“装饰者”后,就可以开始实现了,按照类图的4个角色 分别来实现:
抽象的接口 角色
首先来看“抽象的接口”,本实例中该接口中只定义了一个render方法:
public interface RenderService { void render(); }
具体的被装饰者 角色
具体的被装饰者角色有两个:PcRenderServcie 、MRenderServcie,实现了pc和m页面渲染的基础操作逻辑:
public class PcRenderService implements RenderService{ public void render() { //省略渲染过程 System.out.println("pc页面渲染"); } } public class MRenderServcie implements RenderService{ public void render() { //省略渲染过程 System.out.println("m页面渲染"); } }
抽象的装饰者 角色
根据上述类图介绍,抽象的装饰者需要继承“抽象的接口”RenderService,并且拥有一个“具体的被装饰者”成员变量:
public abstract class AbstractDecorator implements RenderService{ protected RenderService renderService; //“具体的被装饰者”也是RenderService类型 protected AbstractDecorator(RenderService renderService){ this.renderService=renderService; } }
具体的装饰者 角色
本示例中有三个具体的装饰者:CacheDecorator(增加redis缓存处理)、CdnDecorator(增加cdn缓存处理)、GetSkuDecorator(增加sku抓取处理)。可以看到装饰者角色的作用 其实就是对被装饰者进行增强,并且可以在运行期选择性的增强。下面分别来看看具体的实现:
//redis缓存装饰器 public class CacheDecorator extends AbstractDecorator { public CacheDecorator(RenderService renderService){ super(renderService); } public void render() { renderService.render(); redisHandle(); } private void redisHandle(){ System.out.println("渲染完成后缓存到redis"); } } public class CdnDecorator extends AbstractDecorator { //cdn缓存装饰器 public CdnDecorator(RenderService renderService){ super(renderService); } public void render() { renderService.render(); cdnHandle(); } private void cdnHandle(){ System.out.println("渲染完成后推送cdn"); } } //页面sku抓取装饰器 public class GetSkuDecorator extends AbstractDecorator { public GetSkuDecorator(RenderService renderService){ super(renderService); } public void render() { renderService.render(); getSku(); } private void getSku(){ System.out.println("渲染完成后抓取页面sku"); } }
到这里“装饰器模式”的4个角色都已实现完毕,下面来进行测试,测试内容为(其实可以任意的组合):首先创建一个带redis缓存+cdn缓存+页面sku抓取的“m页面”;再创建一个带redis缓存+cdn缓存的“pc页面”。测试代码:
public class Main { public static void main(String[] args) { //渲染一个 redis缓存+cdn缓存+sku抓取的 m页面 RenderService mRenderServcie = new MRenderServcie(); mRenderServcie = new CacheDecorator(mRenderServcie); mRenderServcie = new GetSkuDecorator(mRenderServcie); mRenderServcie = new CdnDecorator(mRenderServcie); mRenderServcie.render(); System.out.println(" "); //渲染一个 redis缓存+cdn缓存 pc页面 RenderService pcRenderServcie = new PcRenderService(); pcRenderServcie = new CacheDecorator(pcRenderServcie); pcRenderServcie = new CdnDecorator(pcRenderServcie); pcRenderServcie.render(); } }
运行上述main方法,执行结果如下:
m页面渲染 渲染完成后缓存到redis 渲染完成后抓取页面sku 渲染完成后推送cdn pc页面渲染 渲染完成后缓存到redis 渲染完成后推送cdn
到这里,也许你已经看到“装饰器模式”的威力(如果还没看到,只能说明我没有讲好)。现在假设需要为pc页面添加头尾,只需要在加一个“头尾处理装饰器”即可,以前的代码完全不用做任何调整。如果需要再渲染一个“微信手Q”页面,只需要新增一个“被装饰者”,可以复用已有的三个“装饰者”进行任意组合的页面渲染。
java.io包
熟悉了“装饰器模式”,我们再回顾一下java的io包中的API。Java的io包中的api结构大致如下:
(注 本图来自百度图片,没有列全)
把上图分为三层:
第一层:Reader、Writer、InputStream、OutputStream,对应“抽象的接口”角色。
第二层:FilterOutputStream、FilterInputStream、FilterWriter、FilterReader这些带有Filter的类对应“抽象的装饰者”角色,这里也不一定要使用接口和抽象类,比如FilterOutputStream就是具体的实现类。这也属于“装饰器模式”。另外InputstreamReader也可以算“抽象的装饰者”,只是这里只对StreamDecoder类型进行装饰。
第二层其他:第二层中除了上述提到的5个类,其他的都可以看做是“具体的被装饰者”。
第三层:第三层都是“具体的装饰者” 比如:PrintStream、DateOutputStream、BufferInputSteam等。可以使用这些装饰器,对第二层中的类进行装饰。
如果把这个图顺时针旋转90度,你会发现它跟“装饰器模式”的类图很相似。
最后简单的看下,java.io流的用法:
public static void main(String[] args) throws Exception {
InputStream inputStream = null;
try{
//使用第二层中的类 创建“被装饰者”
inputStream = new FileInputStream("D://persion.txt");
//使用第三层中的类 创建两个“具体的装饰者”
inputStream = new BufferedInputStream(inputStream);//添加buffer读功能
inputStream = new DataInputStream(inputStream);//添加读取基本java数据类型功能
//开始读文件
byte[] bs = new byte[inputStream.available()];
inputStream.read(bs);
String content = new String(bs);
System.out.println(content);
}finally{
inputStream.close();
}
}
可以看到跟上述“装饰器模式”示例中的用法是一样的。想添加什么功能,就加什么装饰器进行包装即可。
小结
装饰器模式 可以在满足“开闭原则”的前提下,对“被装饰者”在运行时进行动态的包装(功能增强),是使用继承进行扩展的另一种选择。但由此也会产生很多“具体的装饰者”小类,让人困惑(比如 java的io包),但如果你已经熟悉了“装饰器模式”,那就不是问题了。
最后需要注意的是有些装饰器,是需要有序的进行包装的,这时最好把这部分独立出来进行复用(比如 可以使用工厂方法模式等),防止包装顺序出错。