上一篇,我们详细讲解了组合模式,回顾一下:其主要将对象组合成树形结构以表示“部分——整体”的层次结构,这样可以使得用户对单个对象和组合对象的使用具有一致性,因为它们都遵循一套相同的接口,无须区别对待;相对于安全式的实现,透明性的实现方式通常是更好的选择,因为它真正符合了组合模式的本质意图。对象组合是组合模式的实现的根本,今天我们将要讲解的模式也同样是通过对象组合的方式来实现,也是将Favor Composition Over Inheritance原则演绎到极致的一种模式,下面就让我们揭开它的神秘面纱吧。
在实际的软件开发中,我们通过会考虑通过继承的方式来扩展对象的功能,但是由于继承为类型增加了静态特质,在未来扩展之时不够灵活、方便,同时随着子类的增加(扩展的功能子类),各种功能子类的组合势必会导致更加子类的出现,呈现出“类爆炸”的棘手场景,不管从管理上还是从扩展上来说,这都会是开发人员的恶梦。如何提供一种封装机制,将“对象功能的扩展”能够根据需求动态的增加和删除?又能很好地避免由于“对象功能的扩展”引入的子类激增的问题?将“功能扩展的需求”所导致的影响降至最低限度,这就是我们今天所要重点讲述的装饰模式的用武之地呢!
动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式相比生成子类更为灵活。
既然是给对象添加额外的职责,自然就不应该改变对象的类型,更不应该改变对象的接口呢。换句话来说就是我们应该是透明地给一个对象增加功能,不能让这个对象知道,也就是不能去改动这个对象。我们只需要为对象添加的职责编写一个类,用于完成这个职责的具体实现,至于对象的核心职责还是委托转调给这个被装饰的对象,这样我们就有效地将类的核心职责与装饰功能(额外功能)区分开呢,也有利于去除重复的装饰逻辑。
抽象组件(Component)角色:组件对象的接口,可以给这些对象动态地添加职责。 具体组件(ConcreteComponent)角色:具体的组件对象,实现组件对象接口,通过就是被装饰器装饰的原始对象,也就是可以给这个对象添加额外职责。 装饰器(Decorator)角色:所有装饰器的抽象父类,需要定义一个与组件接口相一致的接口,并持有一个Component对象,本质上就是持有一个被装饰的对象。 具体装饰器(ConcreteDecorator)角色:实际的装饰器对象,实现具体的要向被告装饰对象添加的额外功能。
从示例代码中,我们可以清楚地理解装饰模式的基本实现方式。首先,抽象组件Component类定义了组件对象的接口,而具体组件ConcreteComponent实现了组件接口,是真正的组件。紧接着是关键的装饰器接口的定义,它继承了抽象组件接口,同时还持有一个组件接口对象的引用。之所以要继承组件接口,目的是为了让装饰对象与组件对象保持相同的接口,也就是类型一致,这样方便客户端无差别地操作组件对象和装饰对象,而持有一组件接口对象的主要原因是为了调用组件的核心职责,毕竟,装饰品的目的主要是给具体的组件对象添加额外职责,核心的功能还是交给组件对象本身来完成。综合二者,可以看出,装饰器接口的定义关键是为透明地给组件对象添加额外功能。在这里,需要强调的是,各个装饰器之间最好是完全独立的功能,彼此间不应该存在依赖,唯有这样,在进行装饰组合的时候,才没有先后顺序的限制,也就是说无论先装饰谁或者后装饰谁结果应该都是一致的,否则将会大大地降低装饰器组合的可扩展性和灵活性。另外,每一个装饰器的功能粒度不应该太大,这样更有利于功能的复用性,在将来的可以通过多个装饰器来组合完成较复杂的额外功能。最后,在示例代码69行处,装饰器decorator2同时具有了两种额外的功能,因为它组合了两种不同的装饰器。这也是装饰模式的精妙之处。
在现实生活场景中,也有不少可以抽象为装饰模式的例子。比如,我们房间里挂的图画,它的主要功能是为了美化我们房间,让进入房间的我们更加赏心悦目,心情愉快。通常为了更好地保存这样一幅图画,我们会为其制作一边框,防止它轻易地被损坏;另外,如果图画由于某种客观原因不适合挂在墙壁上,我们也会考虑为其制作一支架,将其固定在房间的某一位置上,防止图画被摔坏等意外事件。从上面的例子中,我们可以看到,不管是为图画制作边框还是制作支架,都只是为图画自身添加额外的功能罢了,其本身的核心功能我们并没有改变,之所以添加这样那样的职能,主要是为了更方便我们对图画的使用和欣赏而已。这里需要提醒的是,为图画添加边框或者支架两者是没有关联或者依赖的,彼此是独立的,也就是说完全可以分开添加亦可以一起添加,这只取决于我们自己的意愿而已。这里,添加边框和支架相对于图画来说就是两个装饰器,用于装饰图画。说了这么多,还是让我们用代码来演绎下其基本的实现过程吧。
1: public abstract class Picture{ 2: public abstract void Show(); 3: } 4: 5: public class Canvas extends Picture{ 6: public void Show(){ 7: System.out.println("Canvas is showing!"); 8: } 9: } 10: 11: public abstract class Decorator extends Picture{ 12: protected Picture picture; 13: public Decorator(Picture picture){ 14: this.picture=picture; 15: } 16: 17: public void Show(){ 18: picture.Show(); 19: } 20: } 21: 22: public class Frame extends Decorator{ 23: public Frame(Picture picture){ 24: super(picture); 25: } 26: 27: private void addFrame(){ 28: System.out.println("frame is added to the picture!"); 29: } 30: 31: public void Show(){ 32: addFrame(); 33: super.Show(); 34: } 35: 36: } 37: 38: public class Carrier extends Decorator{ 39: public Carrier(Picture picture){ 40: super(picture); 41: } 42: 43: private void makeCarrier(){ 44: System.out.println("Carrier is made for picture!"); 45: } 46: 47: public void Show(){ 48: makeCarrier(); 49: super.Show(); 50: } 51: } 52: 53: 54: public class Client{ 55: public static void main(String[] args){ 56: Picture picture=new Canvas(); 57: Decorator frame=new Frame(picture); 58: frame.Show(); 59: 60: Decorator carrier=new Carrier(frame); 61: carrier.Show(); 62: } 63: }可以看到,上面的代码与上文的示例代码结构上几乎保持一致,这里,我们也就不再重复叙述呢。提醒一点是,代码60行,装饰器对象carrier此时已经同时拥有了两项额外职责,添加边框和添加支架的功能,因为它对直接对Frame装饰器进行了装饰,自然也就拥有了边框装饰器所支持的功能呢。下图是示例的类结构图:
在java语言的世界里,I/O流应该是装饰模式最典型的应用之一呢。回忆一下,我们通过会如何使用流式操作读取文件内容的呢?下面是简单的代码示例:
1: public static void main(String[] args) throws IOException{ 2: DataInputStream din=null; 3: try { 4: din=new DataInputStream( 5: new BufferedInputStream( 6: new FileInputStream("Test.txt"))); 7: 8: byte bs[]=new byte[din.available()]; 9: din.read(bs); 10: String content=new String(bs); 11: System.out.println("文件内容为: "+content); 12: } finally{ 13: din.close(); 14: } 15: }从上述代码中,我们可以看到,最底层的FileInputStream外层被两个装饰器装饰着,一个是DataInputStream,一个是BufferedInputStream。FileInputStream对象相当于最原始的被装饰组件对象,而BufferedInputStream对象和FileInputStream对象则相当装饰器,示例代码其实就是装饰器的组装过程。大家可能对java中的I/O结构体系并不清楚,可以明确的是,既然I/O流可以通过装饰器模式来组装,那就说明装饰器与具体的组件类要实现相同的接口,下面的类结构图便是java中I/O类图关系,通过它,大家应该就很清楚呢,与装饰模式结构图基本一致,这里我们省去了各个类中接口方法。
从上图,我们可以发现,I/O类结构与装饰模式结构几乎是一样的:
InputStream就相当于装饰模式中的Component FileInputStream、ObjectInputStream、StringBufferInputStream都实现了InputStream接口,所以它们相当于装饰模式的具体组件类(ConcreteComponent)。 FilterInputStream不仅实现了InputStream接口,还持有InputStream接口对象引用,其实就是装饰模式的Decorator角色,而继承于FilterInputStream的DataInputStream、BufferedInputStream、LineNumberInputStream、PushbackInputStream就是具体的装饰器对象。好呢,对装饰模式的现实场景举例就说到这呢,相信通过上面两个例子,大家应该也比较清楚装饰模式的基本使用呢。
装饰模式的本质是:动态组合。动态是手段,组合才是目的。这里的组合有两层意义,一个是动态功能的组合,也就是动态进行装饰器的组合;另一个是指对象组合,通过对象组合来为被装饰对象透明地增加功能。此外装饰模式不仅可以增加功能,亦可以控制功能的访问,完全实现新的功能,同时也可以控制装饰的功能是在装饰功能之前还是之后立即来运行等。总之,装饰模式是通过把复杂功能简单化,分散化,然后在运行期间,根据需要动态组合相应的装饰器,获取相应的职责,这也是为什么需要将装饰器功能尽量细粒度化的原因,有利于复用。到这里,对装饰模式的讲解也已经接近尾声呢,下一篇,我们将继续学习另一种结构型模式——外观模式,敬请期待!
参考资料:
程杰著《大话设计模式》一书 陈臣等著《研磨设计模式》一书 GOF著《设计模式》一书 Terrylee .Net设计模式系列文章 吕震宇老师 设计模式系列文章转载于:https://www.cnblogs.com/JackyTecblog/archive/2012/10/09/2716662.html
