装饰着模式拥有一个设计非常巧妙的结构,它可以动态添加对象功能。在基础的设计原则中,有一条重要的设计准则叫合成/聚合复用原则。根据该原则的思想,代码复用应该尽可能使用委托,而不是使用继承。因为 继承是一种紧密耦合,任何父类的改动都会影响其子类,不利于系统维护。而委托则是松耦合,只要接口不变,委托类的改动并不会影响其上层对象。
装饰着模式就充分运用了这种思想,通过委托机制,复用系统中的各个组件,在运行时,可以将这些功能组件进行叠加,从而构建一个“超级对象”,使其拥有所有这些组件的功能。而各个子功能模块,被很好地维护在各个组件的相关类中,拥有整洁的系统结构。
装饰着模式这种结构可以很好地将功能最难喝性能组件进行分离,彼此互不影响,并在需要的时候,有机地结合起来。为了跟好地理解装饰着模式如何做到性能模块地分离,首先,需要对装饰着模式做一个总体对了解。
装饰着模式的基本机构如下图:
装饰者(Decorator)和被装饰者(ConcreteComponent)拥有相同的接口Component。
被装饰者通常是系统的核心组件,完成特定的功能目标。而装饰者则可以在被装饰者的方法前
后,加上特定的前置处理和后置处理,增强被装饰者的功能。
装饰者模式的主要角色如表:
装饰者模式的一个经典案例就是对输出结果进行增强。比如, 现在需要将一结果通过HTML进行发布,那么首先就需要将内容转化为一个HTML头。同时,由于内容需要在网络上通过HTTP流传,故,还需要为其增加HTTP头。当然作为一个更复杂的情况,可能还要为其安置TCP头等。但作为一个实例,这里做简化处理。
装饰者模式但核心思想在于:无需将所有的逻辑,即,核心内容构建、HTML文本构造和HTTP头生成等3个功能模块粘合在一起实现。通过装饰着模式,可以将他们分解为3个几乎完全独立的组件,并在使用时灵活地进行装配。为实现这个功能,可以使用如图所示结构:
IPacketCreator即装饰接口,用于处理具体的内容。PacketBodyCreator是具体的组件,它的功能是构造要发布信息的核心内容,但是它不负责将其构造成一个格式工整、可直接发布的数据格式。PacketHTTPHeaderCreator负责对给定的内容加上HTTP头部,PacketHTMLHeaderCreator负责将给定的内容格式化成HTML文本。如上图所示,3个功能模块相对独立且分离,易于系统维护。
IPacketCreator的实现很简单,他是一个但方法的接口:
public interface IPacketCreator { public String handleContent(); //用于内容处理 }
PacketBodyCreator 用于返回数据包的核心数据:
public class PacketBodyCreator implements IpacketCreator { @Override public String handleContent() { return "Content of Packet"; //构造核心数据,但不包括格式 } }
PacketDecorator维护核心组件component对象,它负责告知其子类,其核心业务逻辑应该全权委托component完成,
自己仅仅是做增强处理。
public abstract class PacketDecorator implements IPacketCreator { IPacketCreator component; public PacketDecorator(IPacketCreator c) { component = c; } }
PacketHTMLHeaderCreator是具体的装饰器,它负责对核心发布的内容进行HTML格式化操作。
需要特别注意的是,他委托了具体组件component进行核心业务处理。
public class PacketHTMLHeaderCreator extends PacketDecorator { public PacketHTMLHeaderCreator(IPacketCreator c) { super(c); } @Override public String handleContent() { StringBuffer sb = new StringBuffer(); sb.append(""); sb.append(""); sb.append(component.handleContent()); sb.append(""); sb.append("\n"); return sb.toString(); } }
PacketHTTPHeaderCreator 与 PacketHTMLHeaderCreator类似,但是它完成数据包HTTP头部的处理。
其余业务处理依然交由内部的component完成。
public class PacketHTTPHeaderCreator extends PacketDecorator { public PacketHTTPHeaderCreator(IPacketCreator c) { super(c); } @Override public String handleContent() { //对给定数据加上HTTP头信息 StringBuffer sb = new StringBuffer(); sb.append("Cache-control:no-cache\n"); sb.append("Date:Mon,31Dec201204:25:57GMT\n"); sb.append(component.handleContent()); return sb.toString(); } }
对于装饰者模式,另一个值得关注的地方是它的使用方法。在本例中,通过层层构造和组装这些装饰者和被装饰者到一个对象中,
使其有机地结合在一起工作。
public class Main { public static void main(String[] args) { IPacketCreator pc = new PacketHTTPHeaderCreator( new PacketHTMLHeaderCreator( new PacketBodyCreator())); System.out.println(pc.handleContent()); } }
可以看到,通过装饰者的构造函数,将被装饰对象传入。本例中,共生成3个对象实例,作为核心组件的PacketBodyCreator ,
最先被构造,其次是PacketHTMLHeaderCreator,最后才是PacketHTTPHeaderCreator。
这个顺序表示,首先由PacketBodyCreator对象去生成核心发布内容,接着由PacketHTMLHeaderCreator对象对这个内容进行处理,将其转化为HTML,最后由PacketHTTPHeaderCreator对PacketHTMLHeaderCreator的输出安装HTTP头部。程序运行结果如下:
下图是本例的调用堆栈,从调用堆栈中,应该可以更容易地理解各个组件的相互关系。
在JDK的实现中,有不少组件也是用装饰着模式实现的。其中,一个最典型的例子就是OutputStream和InputStream类族的实现。以OutputStream为例,OutputStream对象提供的方法比较简单,功能也比较挼2,但通过各种装饰者但增强,OutputStream对象可以被赋予强大的功能。
下图显示以OutputStream为核心的装饰者模式的实现。其中FileOutputStream为系统的核心类,它实现了向文件写入数据。使用DataOutputStream可以在FileOutputStream的基础上,增加对多种数据类型的写操作支持,而BufferedOutputStream装饰器,可以对FileOutputStream增加缓冲功能,优化I/O的性能。以BufferedOutputStream为代表的性能组件,是将性能模块和功能模块分离的一种典型实现。
public static void main(String[] args) throws IOException { //生成一个有缓冲功能的流对象 //spend:21 DataOutputStream dout = new DataOutputStream( new BufferedOutputStream( new FileOutputStream("C:\\a.txt"))); //没有缓冲功能的流对象 //spend:592 DataOutputStream dout1 = new DataOutputStream( new FileOutputStream("C:\\a.txt")); long begin = System.currentTimeMillis(); for (int i =0;i<100000;i++){ dout.write(i); //dout1.write(i); } System.out.println("spend:" + (System.currentTimeMillis() - begin)); }
以上代码显示FileOutputStream的典型应用。加粗部分是两种建立OutputStream的方法,第一种假如了性能组件BufferedOutputStream,第二种则没有。因此,第一种方法产生的OutputStream拥有更好的I/O性能。
下面看一下装饰者模式如何通过性能组件增强I/O性能。在运行时,工作流程如图:
在FileOutputStream.write()的调用之前,会首先调用BufferedOutputStream.write(),它的实现如下:
可以看到,并不是每次BufferedOutputStream.write()调用都会去磁盘写入数据,而是将数据写入缓存中,当缓冲写满时,才调用FileOutputStream.write()方法,实际写入数据。以此实现性能组件与功能组件的完美分离。