1 装饰模式概念
1.1 装饰者模式定义
定义:装饰模式的基本含义是能够动态地为一个对象添加一些额外的行为职责。
谈到对象行为职责的扩展,我们很容易就能够想到面向对象编程语言的一个重要特征:集成。继承是绝大多数面向对象的编程语言在语法上的天然支持。通过使用继承,我们可以获取以下两种扩展特性:
- 现有对象行为的覆盖——通过覆写(Override)父类中的已有方法完成。
- 添加新的行为职责——通过在子类中添加新的方法完成。
但是继承这个语法,为对象类型引入的是一种“静态”特性扩展。这一扩展在行为特性的获取在“编译期”就被决定了,而并非是一个“运行期”的扩展模式。随着子类的增多,虽然我们获得了更多的扩展功能,然而各种子类的组合(扩展功能的组合)将导致子类的极度膨胀。在java语言中,一个类智能进行“单根继承”而无法支持“多重继承”,因而通过“继承”这种方式进行功能行为特性的扩展缺乏足够的灵活性。后面,通过例子来详细说明下这个的严重性。
装饰者模式体现了软件设计一个非常重要的设计原则:类应该对扩展开放,对修改关闭。
1.2 装饰者模式组成
图1 装饰模式的一种基本实现示意图
从图1中我们可以归纳出装饰模式的基本实现中所涉及的角色,用人穿衣服类比喻吧:
原始接口(Component)——定义了一个接口方法。用来规范的。
模式目标实现类(ConcreteComponent)——对于原始接口的默认实现方式。在装饰模式中,默认目标实现类被认为是有待扩展的类,其方法operation被认为是有待扩展的行为方法。就是要被装饰的类,就是人。
装饰实现类(Decorator)——同样实现了原始接口,既可以是一个抽象类,也可以是一个具体实现类。其内部封装了一个原始接口的对象实例:ConcreteComponent,这个实例的实现往往被初始化成默认目标实现类(ConcreteComponent)。衣服这个概念!
具体装饰实现类(ConcreteDecorator)——继承自装饰实现类(ComponentDecorator),我们可以再operation方法中调用原始接口的对象实例ConcreteComponent获得默认目标实现类的行为方式并在其中加入行为扩展实现,也可以添加自由添加新的行为职责addBehaviorA等。自然是内衣之类的!
1.3 应用场景
装饰者模式以对客户端透明的方式扩展对象功能,是集成的一种替代方案。就是我们动态的给一个对象附加功能时,客户端是感觉不出来的,代码完全不会有任何改变。
装饰者模式可以在不创造更多子类的情况下,将对象的功能扩展。
装饰模式具备了一些比“继承”更加灵活的应用场景:
- 适合对默认目标实现中的多个接口进行排列组合调度
- 适合对默认目标实现进行选择性扩展(java.io.InputStream)
- 适合对默认目标实现未知或者不易扩展的情况(HttpServletResponseWrapper)。
2 JAVA类库中的策略模式
2.1 Java.io.Inputstream
2.1.1 日常使用中的装饰者模式
Java.io一个简单的例子
1 public static void main(String[] args) throws IOException { 2 DataOutputStream dos = new DataOutputStream(new BufferedOutputStream( 3 new FileOutputStream("data.txt")));//关键点 4 5 byte d = 3; 6 int i = 14; 7 char ch = 'c'; 8 float f = 3.3f; 9 10 dos.writeByte(d); 11 dos.writeInt(i); 12 dos.writeChar(ch); 13 dos.writeFloat(f); 14 15 dos.close(); 16 17 DataInputStream dis = new DataInputStream(new BufferedInputStream( 18 new FileInputStream("data.txt")));//关键点 19 20 System.out.println(dis.readByte()); 21 System.out.println(dis.readInt()); 22 System.out.println(dis.readChar()); 23 System.out.println(dis.readFloat()); 24 25 dis.close(); 26 27 }
结果:
图2 读取结果和二进制文件
在关键点,FileIutputStream("data.txt")这个就是默认的目标实现类。FileIutPutStream是最基本的吧,读取文件的一个类。然后对它进行了两成包装,第一层BufferedIutputStream,这个包装后输入类就有缓存的功能;第二层DataIutputStream,这个包装后输出类就有了数据类型的功能。在不知不觉中,我们一直用着装饰者模式。
在java.io.inputstream的装饰者模式如下所示:
图3 InputStream的装饰者模式示意图(部分)
我们在图3中展示了部分的InputStream,FileInputStream、ByteArrayInputStream、StringBufferInputStream是属于目标实现类,分别从文件系统中获取字节流、从流中读取字节、以字符串的方式获取字节流。默认目标实现类在这里是源,是最基本的东西。实现FilterInputStream的几个类,便是具体装饰实现类,分别可以添加缓存、校验、数据类型、跟踪行号的功能。
所以使用java.io的时候,我们只要记住这种模式就好:
new ConcreteDecoratorA(new ConcreteDecoratorB(new ConcreteComponent));
包装几层我不管,只要最里面的是一个默认目标实现类就可以了,这样java.io的用法是否就简单易记了啊!要不然,这么多类还不搞死人。这里我们是不是就体现了装饰者模式的第二种使用场景,适合对默认目标实现进行选择性扩展,那些附加功能你爱怎么选怎么选。
2.1.2 InputStream源码分析
1.原始接口(InputStream)
1 public abstract class InputStream implements Closeable { 2 public abstract int read() throws IOException; 3 //read(byte b[])、read(byte b[], int off, int len)调用的还是read() 4 }
这是一个抽象类,在这个抽象类中只有一个抽象方法read(),也就是一定要用子类去实现的。不过别的函数会调用这个方法。所以,InputStream的所有子类都必须实现read()。
2.默认目标实现类(FileInputStream)
1 class FileInputStream extends InputStream 2 { 3 public native int read() throws IOException; 4 }
这个默认目标实现类里面,实现了父类InputStream中的抽象方法,当然用的是native本地方法我是看不到的。不过,肯定是read()方法已经实现了的。
3. 装饰实现类(FilterInputStream)
1 class FilterInputStream extends InputStream { 2 protected volatile InputStream in; 3 /*装饰者模式里面最重要的特征1,默认目标实现类封装于装饰实现类或者其子类的内部,从而形成对象之间的引用关系。 4 简单的说,装饰实现类(FilterInputStream)一定是继承或实现原始接口(InputStream)的,内部有包含一个原始接口的超类(其实就是某个默认目标实现类)。 5 */ 6 protected FilterInputStream(InputStream in) { 7 this.in = in; 8 } 9 /*一定会提供一个口子,来初始化目标这个超类(in)。说穿了,装饰实现类(衣服),没有这个目标实现类(人)我什么都干不了,具体是哪个人跟我衣服就没关系了,你穿给我就好*/ 10 }
装饰实现类,是整个装饰者模式里面的核心,应该说是一看就看出是装饰者模式的地方。两个重要特征:
- 默认目标实现类封装与装饰实现类(ComponentDecorator)或者其子类的内部,从而形成对象之间的引用关系
- 装饰实现类(ComponentDecorator)同样实现了原始接口(Component)
4.具体装饰实现类(BufferedInputStream、DataInputStream)
BufferedInputStream源码
1 public class BufferedInputStream extends FilterInputStream { 2 public BufferedInputStream(InputStream in) { 3 this(in, defaultBufferSize); 4 } 5 6 public BufferedInputStream(InputStream in, int size) { 7 super(in);//装饰者类里面现在又具体的目标实现类了 8 if (size <= 0) { 9 throw new IllegalArgumentException("Buffer size <= 0"); 10 } 11 buf = new byte[size]; 12 } 13 //返回的就是刚刚传进去的目标实现类FileOutputStream("data.txt") 14 private InputStream getInIfOpen() throws IOException { 15 InputStream input = in; 16 if (input == null) 17 throw new IOException("Stream closed"); 18 return input; 19 } 20 private void fill() throws IOException { 21 byte[] buffer = getBufIfOpen(); 22 //这里本来还有n多内容,处理缓存大小,位置之类的 23 int n = getInIfOpen().read(buffer, pos, buffer.length - pos); 24 //上面这句非常重要,getInIfOpen()返回的是in(其实就是FileOutputStream("data.txt")),然后调用了目标实现类里面的方法,放到了buffer这个缓存中。 25 if (n > 0) 26 count = n + pos; 27 } 28 public synchronized int read() throws IOException { 29 if (pos >= count) { 30 fill(); 31 if (pos >= count) 32 return -1; 33 } 34 return getBufIfOpen()[pos++] & 0xff; 35 }
DataInputStream源码
1 public class DataInputStream extends FilterInputStream implements DataInput { 2 public DataInputStream(InputStream in) { 3 super(in); 4 } 5 //int,四个4字节,每次读取一个字节,只要把这四个字节拼装成int的顺序,就成为一个int了。 6 public final int readInt() throws IOException { 7 int ch1 = in.read();//每次读一个字节 8 int ch2 = in.read(); 9 int ch3 = in.read(); 10 int ch4 = in.read(); 11 if ((ch1 | ch2 | ch3 | ch4) < 0) 12 throw new EOFException(); 13 return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0)) 14 }
现在让我们来回顾下,我们的例子中dis.readInt(),调用了DataInputStream里面的readInt(),in.read()调用了BufferInputStream中的read,然后BufferInputStream里面又调用了FileInputStream里面的本地方法read()。这样就完成了仅仅读取一个字节,到把读取过的缓存起来,再到客户端看起来是读取了一个int,这样华丽的转变。
在这个应用里面,我们调用的顺序是有一定要求的。就缓存这个东西为例吧!如果我们用的是继承的方式,那么带类型的输入,就至少有两个子类DataInputStream和BufferDataInputStream。然后,别的LineNumberInputStream,这种类是不是也要缓存不缓存的分。如果,还有别的功能呢,带检查不带检查,这样组合的话,子类是不是会急剧膨胀。
OutputStream和InputStream没有本质上的区别,所以这里就不讲了。
2.2 HttpServletRequestWrapper
2.2.1 Servlet编程中的装饰者模式
SampleRequsetWrapper类
1 public class SampleRequestWrapper extends HttpServletRequestWrapper{ 2 3 public SampleRequestWrapper(HttpServletRequest request) { 4 super(request); 5 } 6 7 public String getParameter(String s){ 8 System.out.println("here is the decorator!"+s); 9 return super.getParameter(s); 10 } 11 }
SampleFilter类
1 public class SampleFilter implements Filter { 2 3 private FilterConfig filterConfig; 4 @Override 5 public void doFilter(ServletRequest request, ServletResponse response, 6 FilterChain chain) throws IOException, ServletException { 7 HttpServletRequest req = (HttpServletRequest) request; 8 req = new SampleRequestWrapper(req); 9 chain.doFilter(req, response); 10 } 11 }
web.xml
<filter> <filter-name>sampleFilter</filter-name> <filter-class>SampleFilter</filter-class> </filter> <filter-mapping> <filter-name>SampleFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
每次使用request. getParameter()时,都会在控制面板里面输出上述结果。这里是简单的输出结果,你也可以加入别的功能如“优先从缓存中读取数据”,“编码设置”等功能。
在HttpServletRequestWrapper的装饰者模式设计框架如图所示:
图4 HttpServletRequestWrapper装饰者模式示意图
我们知道,HttpServletRequset和HttpServletResponse是Servlet标准所指定的Java语言与Web容器进行交互的接口。接口本身只规定java语言对web容器进行访问的行为方式,而具体的实现是由不同的web容器在其内部实现的(上图request就是tomcat的实现)。
那么在运行期,当我们需要对HttpServletRequset和HttpServletResponse的默认实例进行扩展时,我们就可以继承HttpServletRequestWrapper和HttpServletResponseWrapper来实现。
2.2.2 HttpServletRequestWrapper源码分析
HttpServletRequestWrapper源码
1 public class HttpServletRequestWrapper extends ServletRequestWrapper implements HttpServletRequest { 2 public HttpServletRequestWrapper(HttpServletRequest request) { 3 super(request); 4 } 5 }
HttpServletRequestWrapper是一个什么都没做的具体装饰实现类(concerteDecorator),不过它即继承了装饰实现类,又实现了HttpservletRequst。这里什么都不做是有道理的,因为要对请求添加何种功能事先是没法知道的,不可能盲目的添加。所以,这种里的装饰模式的应用场景:适合对默认目标实现未知或者不易扩展的情况。
ServletRequestWrapper源码
1 public class ServletRequestWrapper implements ServletRequest { 2 private ServletRequest request; 3 4 public ServletRequestWrapper(ServletRequest request) { 5 if (request == null) { 6 throw new IllegalArgumentException("Request cannot be null"); 7 } 8 this.request = request; 9 }
这个类很明显就是装饰实现类(Decorator),提供了一个可以ServletRequest(Component)的构造函数,并在构造函数中实现了将原始ServletRequset接口的实现封装于内部的基本逻辑。这样一来,构成装饰模式的两大基本要素也就全齐了。
我们进一步思考,还能挖掘出装饰者模式作为对象行为的扩展方式比继承更为合理的地方:虽然装饰模式产生的初衷是装饰类对默认目标实现类的行为扩展,然后却并不对默认目标实现类形成依赖。
由于在装饰类内部封装的是接口而并不是默认目标实现类,这样一来,我们在实现目标时,甚至无须知道具体的实现类是谁。如果,上面的例子用继承方式来实现,你就不得不知道使用的是哪种web容器,并且要知道具体会是哪个类实现了HttpServletRequest或HttpServletResponse。
3 总结
装饰者模式应用场景
- 适合对默认目标实现中的多个接口进行排列组合调度
- 适合对默认目标实现进行选择性扩展
- 适合对默认目标实现未知或者不易扩展的情况。
装饰者模式优点
- 通过组合而非继承的方式,实现了动态扩展对象的功能的能力。
- 有效避免了使用继承的方式扩展对象功能而带来的灵活性差,子类无限制扩张的问题。
- 充分利用了继承和组合的长处和短处,在灵活性和扩展性之间找到完美的平衡点。
- 装饰者和被装饰者之间虽然都是同一类型,但是它们彼此是完全独立并可以各自独立任意改变的。
- 遵守大部分GRASP原则和常用设计原则,高内聚、低偶合。
装饰者模式缺点
- 性能影响,这么多的多态和调用,肯定好不了。
- 装饰者会导致设计中出现许多小对象,如果过度使用,会让程序变的很复杂。
4 参考资料
[1]. 《Head First设计模式》
[2]. 《Struts2技术内幕》