一、概念
动态的给一个对象添加一些额外的职责,是继承关系的一个替代方案。
二、模式动机
有时希望给某个对象而不是整个类添加一些功能,我们需要精确的控制给这个对象添加功能的时机。使用继承机制也是添加功能的一种方法,但是它是静态的。如现在有一个类A和一个类B,如果类B产生的对像需要具备类A的功能,那么一种方法就是将类B继承于类A (Class B extends A ),那么类B的对象都将具有类A的功能,而无法做到在某些情况下类B的对象具有类A的功能,某些情况下不具备类A的功能(即类B的对象需要具有类A的功能时,可以动态的添加到类B的对象上)。而装饰模式就可以很好的解决这个问题,且对客户端是透明的。
三、模式的结构
角色职责:
Component:组件对象的接口,能给这些对象动态的添加职责
ConcreteComponent:具体组件对象,实现组件对象的接口,通常就是被装饰的原始对象,也就是可以给这个对象添加职责。
Decorator:所有装饰器的抽象父类,需要定义一个与组件接口一致的接口,并持有一个componet成员,即被装饰的对象,该对象有可能已不是被装饰的原始对象,有可能是已被装鉓过的对象。
ConcreteDecoratorA:具体的装饰器对象,实现具体要向被装饰对象添加功能。
代码样例 :
package decorator.base; /** * 组件对象的接口,能给这些对象动态的添加职责 * @ClassName: Component * @author beteman6988 * @date 2018年1月20日 上午9:13:22 * */ public interface Component { public void operation(); } package decorator.base; /** * 需要添加职责的原始对象 * @ClassName: ConcreteComponent * @author beteman6988 * @date 2018年1月20日 上午9:28:52 * */ public class ConcreteComponent implements Component { @Override public void operation() { // 具体业务 System.out.println("对象的原始职责!"); } } package decorator.base; /** * 装饰器的抽象父类,持有一个指上组件对象的接口对象,并定义一个与组件接口一致的接口 * @ClassName: Decorator * @author beteman6988 * @date 2018年1月20日 上午9:37:02 * */ public abstract class Decorator implements Component { protected Component component; //指上组件对象的接口对象 public Decorator(Component component) { this.component = component; } public void operation() { this.component.operation(); //转发请收给组件对象 } } package decorator.base; /** * 具体装饰器,为组件对象添加职责 * @ClassName: ConcreteDecoratorA * @author beteman6988 * @date 2018年1月20日 上午9:53:47 * */ public class ConcreteDecoratorA extends Decorator { public ConcreteDecoratorA(Component component) { super(component); } public void operation() { //调用父类的方法之前添加职责 System.out.print("添加职责A + "); super.operation(); //转调父类的方法 //调用父类的方法之后可以添加职责 } } package decorator.base; /** * 具体装饰器,为组件对象添加职责 * @ClassName: ConcreteDecoratorB * @author beteman6988 * @date 2018年1月20日 上午9:52:21 * */ public class ConcreteDecoratorB extends Decorator { public ConcreteDecoratorB(Component component) { super(component); } public void operation() { //调用父类的方法之前添加职责 System.out.print("添加职责B + "); super.operation(); //转调父类的方法 //调用父类的方法之后可以添加职责 } } package decorator.base; public class Client { public static void main(String[] args) { Component component=new ConcreteComponent(); //原始组件对象 Component componentA=new ConcreteDecoratorA(component); //给原始组件对象添加职责A componentA.operation();//这时componentA对象就具有了组件A的职责和原始组件的职责 Component componentB=new ConcreteDecoratorB(component); //给原始组件对象添加职责B componentB.operation(); //这时componentB对象就具有了组件B的职责和原始组件的职责 Component componentC=new ConcreteDecoratorB(componentA); //给组件A对象添加职责B componentC.operation(); //这时componentC对象就具有了组件B的职责+组件A的职责+原始组件的职责 } }
运行结果:
添加职责A + 对象的原始职责!
添加职责B + 对象的原始职责!
添加职责B + 添加职责A + 对象的原始职责!
半透明的装饰模式
如代码样例所示,针对客户端使用的只是一个Component的接口对象,对客户完全是透明的。而这样纯粹的装饰模式在现实生活中其实很难找到的,装饰模式的用意是在不改变接口的前提下,增强所考虑的类的功能。在增强功能的时候,往往需要增加接口中并没有声明过的新的方法 ,换言之,也就是允许装饰模式改变接口,这就是半透明的装饰模式。下面以JDK中的I/O类进行分析如下:
角色分析:
Component: InputStream组件
ConcreteComponent:FileInputStream,读取Byte文件流
Decorator:FilterInputStream ,抽像装饰类
ConcreteDecorator:LineNumberInputStream ,带跟踪行号的字节输入流,可以通过getLineNumber()得到当前输入流的行号
从上面可以看出LineNumberInputStream 是一个半透明的装饰模式,getLineNumber()方法在InputStream接口组件中并不存在,如果想用这个方法就必须用具体的LineNumberInputStream 类,如下例子:
import java.io.FileInputStream; import java.io.InputStream; import java.io.LineNumberInputStream; public class TestLineNumberInputStream { public static void main(String[] args) { try { InputStream is=new FileInputStream("i:\\aa.txt"); LineNumberInputStream lis=new LineNumberInputStream(is); //InputStream lis=new LineNumberInputStream(is); /** 如果这样定义,则下面的int lineNum=lis.getLineNumber(); 将无法使用 因为 InputStream并没有这个方法 boolean flag=true; do{ byte[] array=new byte[1024]; int lineNum=lis.getLineNumber(); for(int i=0;i<1024;i++) { byte b= (byte) lis.read(); if(b== -1 ||b==10 ) { String s=new String(array,"GBK"); System.out.println("第"+lineNum+"行"+" : "+s); if(b== -1) { flag=false; } break; }else { array[i]=b; } } }while(flag); } catch (Exception e) { e.printStackTrace(); } } }
运行结果为:
模式简化一、
如果只有一个ConcreteComponent,那么就没有必要定义一个Component组件接口,这时Decorator直接继承至ConcreteComponent即可,如下图:
模式简化二、
如果具体装饰角色只有一个或者不多于两个,那么Decorator也是可以省略的,如下图所示:
四、模式样例
通过JAVA I/O中的Reader、InputStreamReader、BufferedReader、LineNumberReader来分析装饰模式:
每个类的角色
Component: Reader ,用于读取字符流的顶层抽象类,继承于该类的子类都具有读取字符流的能力。
ConcreteComponent:InputStreamReader,它是字节流到字符流的转换类,是字节流通上字符流的桥梁,它使用指定的charset读取字节并将其解码为字符。它的Read()方法可以每次读取一个字符(每个文件在磁盘上都是以一定的二进制序列进行存储,以什么样的二进制进行存储,则要跟据文件存盘时选定的编码来决定,如一个文本文件中只有一个“a”字符,如果以utf-8 编码进行存储,那么他在磁盘上的二进制编码为“1100001”,一个byte8个bit位。如果以utf-16编码进行存储,则二进制为“11111110111111110000000001100001”,4个字节32个bit位,其中前2个字节16个bit位表示大小尾序。InputStreamReader首先通过传给他的InputSream对象读取到该InputStream中的二进制Byte位,如果将这些二进制的字节转换成正确的字符,那么就必须用存盘时的编码进行逆向解码,所以InputStreamReader的构造函数需要指定解码用的字符集,如果没有指定则用平台默认字符集进行解码)。
Decorator:BufferedReader,具有缓冲功能的字符流读取类,在上面的类图中我们看不到他持有的一个Reader接口类成员,因为它的Reader接口类成员是私有的。但它确实拥有一个Read接口类成员,该类可以看做一个Decorator,因为他是一个非抽象类,它也可以看做一个ConcreteDecorator,从他的做用“具有缓冲功能的字符流读取类”就可以看出,它对它持有的Reader接口对象增加了缓冲功能,如它拥有readLine方法,可以一行一行的的读取字符。
ConcreteDecorator:LineNumberReader,可以在字符流读取时,得到字符所在行号。如上面的类图,它可以给InputStreamReader增加读取行号功能。
综合分析:
Reader抽像类,字符流的父类,继承于它的子类都可以以字符方式进行读取字符。
InputStreamReader:可以从二进制流中以单个字符的方式读取字符.
BufferedReader:对持有的Reader接口对象进行装饰,增加缓冲功能,可以一行一行的读取字符。
LineNumberReader:具体装饰对象,增加读取行号的功能。
public class TestInputStreamReader { public static void main(String args[]) { try { InputStream is=new FileInputStream("i:\\aa.txt"); Reader isReader=new InputStreamReader(is,"GBK"); //将BufferedRead看成ConcreteDecorator /*BufferedReader br=new BufferedReader(isReader); //BufferedReader对InputStreamReader进行装饰 String aLine=br.readLine(); System.out.println(aLine); aLine=br.readLine(); System.out.println(aLine);*/ //将BufferedRead看成Decorator LineNumberReader lineNumReader=new LineNumberReader(isReader); int lineNum=lineNumReader.getLineNumber(); String s=lineNumReader.readLine(); System.out.println("第"+lineNum+"行:"+s); lineNum=lineNumReader.getLineNumber(); s=lineNumReader.readLine(); System.out.println("第"+lineNum+"行:"+s); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
运行结果如下:
第0行:我是中国人
第1行:我深深的爱着我的祖国
五、与其它模式的关系
装饰模式和适配器模式:二者都有一个别名---wrapper模式,但是这两个模式是很不一样的模式,适配器模式的用意是要改变所考虑对象的接口而不一定要改变对象的性能,适配器模式把一个API转换在另一个API,而装饰模式的用意是要保持接口,即保持被包装对象的API,从而增强所考虑对象的性能。
六、模式优缺点
优点:
1.比继承更加灵活:继承是静态,而且一旦继承所有的子类都有一样的功能,而装饰模式模式采用把功能分离到每个装饰器中,然后通过对象组合的方式,在运行时动态的组合功能,每个被装饰的对象最终有哪些功能,是由运行期动态组合的功能来决定的。
2.更加容易复用:可以给一个对象增加多个同样的装饰器,也可以用一个装饰来装饰不同的对象,从而实现复用装饰器的功能。
缺点:因为一般一个装饰器只实现一个功能,为了增强一个对象的功能可能需要很多不同功能的装饰器进行多次的装饰,所以会产生很多细粒度的对象。