2012年10月9日星期二

装饰模式结构型模式(4)

装饰模式结构型模式(4)

前言

上一篇,我们详细讲解了组合模式,回顾一下:其主要将对象组合成树形结构以表示“部分——整体”的层次结构,这样可以使得用户对单个对象和组合对象的使用具有一致性,因为它们都遵循一套相同的接口,无须区别对待;相对于安全式的实现,透明性的实现方式通常是更好的选择,因为它真正符合了组合模式的本质意图。对象组合是组合模式的实现的根本,今天我们将要讲解的模式也同样是通过对象组合的方式来实现,也是将Favor Composition Over Inheritance原则演绎到极致的一种模式,下面就让我们揭开它的神秘面纱吧。

动机

在实际的软件开发中,我们通过会考虑通过继承的方式来扩展对象的功能,但是由于继承为类型增加了静态特质,在未来扩展之时不够灵活、方便,同时随着子类的增加(扩展的功能子类),各种功能子类的组合势必会导致更加子类的出现,呈现出“类爆炸”的棘手场景,不管从管理上还是从扩展上来说,这都会是开发人员的恶梦。如何提供一种封装机制,将“对象功能的扩展”能够根据需求动态的增加和删除?又能很好地避免由于“对象功能的扩展”引入的子类激增的问题?将“功能扩展的需求”所导致的影响降至最低限度,这就是我们今天所要重点讲述的装饰模式的用武之地呢!

意图

动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式相比生成子类更为灵活。

既然是给对象添加额外的职责,自然就不应该改变对象的类型,更不应该改变对象的接口呢。换句话来说就是我们应该是透明地给一个对象增加功能,不能让这个对象知道,也就是不能去改动这个对象。我们只需要为对象添加的职责编写一个类,用于完成这个职责的具体实现,至于对象的核心职责还是委托转调给这个被装饰的对象,这样我们就有效地将类的核心职责与装饰功能(额外功能)区分开呢,也有利于去除重复的装饰逻辑。

结构图

image

  1. 抽象组件(Component)角色:组件对象的接口,可以给这些对象动态地添加职责。
  2. 具体组件(ConcreteComponent)角色:具体的组件对象,实现组件对象接口,通过就是被装饰器装饰的原始对象,也就是可以给这个对象添加额外职责。
  3. 装饰器(Decorator)角色:所有装饰器的抽象父类,需要定义一个与组件接口相一致的接口,并持有一个Component对象,本质上就是持有一个被装饰的对象。
  4. 具体装饰器(ConcreteDecorator)角色:实际的装饰器对象,实现具体的要向被告装饰对象添加的额外功能。

代码示例

 1:  public abstract class Component{
 2:      public abstract void Operation();
 3:  }
 4:   
 5:  public class ConcreteComponent extends Component{
 6:      public void Operation(){
 7:          System.out.println("ConcreteComponent is doing!");
 8:      }
 9:  }
10:   
11:  public abstract class Decorator extends Component{
12:      protected Component component;
13:   
14:      public Decorator(Component component){
15:          this.component=component;
16:      }
17:   
18:      public void setComponent(Component component) {
19:          this.component = component;
20:      }
21:   
22:      public void Operation(){
23:          component.Operation();
24:      }
25:  }
26:   
27:  public class ConcreteDecoratorA extends Decorator{
28:      public ConcreteDecoratorA(Component component){
29:          super(component);
30:      }
31:      private String addState;
32:      public String getAddState() {
33:          return addState;
34:      }
35:      public void setAddState(String addState) {
36:          this.addState = addState;
37:      }
38:   
39:      public void Operation(){
40:          //调用相关状态信息
41:          getAddState();
42:          component.Operation();
43:      }
44:  }
45:   
46:  public class ConcreteDecoratorB extends Decorator{
47:      public ConcreteDecoratorB(Component component){
48:          super(component);
49:      }
50:   
51:      public void Addhavior(){
52:          System.out.println("add a Behavior!");
53:      }
54:   
55:      public void Operation(){
56:          component.Operation();
57:          Addhavior();
58:      }
59:  }
60:   
61:  public class Client{
62:      public static void main(String[] args){
63:          Component component=new ConcreteComponent();
64:   
65:          Decorator decorator1=new ConcreteDecoratorA(component);
66:          decorator1.Operation();
67:   
68:          //直接给装饰对象进行装饰,这样decorator2就同时具有两个额外功能呢
69:          Decorator decorator2=new ConcreteDecoratorB(decorator1);
70:          decorator2.Operation();    
71:      }
72:  }

从示例代码中,我们可以清楚地理解装饰模式的基本实现方式。首先,抽象组件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装饰器进行了装饰,自然也就拥有了边框装饰器所支持的功能呢。下图是示例的类结构图:

 image

在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类图关系,通过它,大家应该就很清楚呢,与装饰模式结构图基本一致,这里我们省去了各个类中接口方法。

 image

从上图,我们可以发现,I/O类结构与装饰模式结构几乎是一样的:

  1. InputStream就相当于装饰模式中的Component
  2. FileInputStream、ObjectInputStream、StringBufferInputStream都实现了InputStream接口,所以它们相当于装饰模式的具体组件类(ConcreteComponent)。
  3. FilterInputStream不仅实现了InputStream接口,还持有InputStream接口对象引用,其实就是装饰模式的Decorator角色,而继承于FilterInputStream的DataInputStream、BufferedInputStream、LineNumberInputStream、PushbackInputStream就是具体的装饰器对象。

好呢,对装饰模式的现实场景举例就说到这呢,相信通过上面两个例子,大家应该也比较清楚装饰模式的基本使用呢。

实现要点

  1. 保持接口的一致性。装饰对象的接口必须与它所装饰品的组件接口保持一致,故所有的具体装饰器都是应该实现同一个公共父类,即Decorator类。而Decorator又继承于抽象组件类Component,因此所有的装饰对象也完全属于组件类型范畴。这样做的好处是Decorator对Component是透明的,Component无须知道Decorator的存在,Decorator是从外部来扩展Component功能。
  2. 可省略抽象Decorator类。当我们仅需要给组件添加一个职责或功能时,完全没必要定义抽象Decorator类。这时,可以直接把Decorator向Component转发请求的职责合并到具体装饰器对象(ConcreteDecorator)中。
  3. 保持Component类的简单性。为了保证接口的一致性,组件和装饰必须有一个公共的Component类。因而,保持这个类的简单性是很重要的,它应该集中定义接口而不是存储数据,对数据表示的定义延迟到子类,否则Component会变得复杂和庞大,也难以使用。关键是赐予Component太多的功能,对于子类来说未必需要,只会造成臃肿的糟糕设计而已。
  4. 改变对象外壳和改变对象内核。Component可以说是组件的内核,而其具体实现由ConcreteComponent来完成,Decorator是组件的外壳,用于改变组件的外在表示和行为。装饰模式主要用于完成对组件外壳的改变,而内核的改变通过是一个改变组件内核的很好模式。不过当Component类原本就比较庞大、复杂时,使用装饰模式代价较高,此时策略模式相对会合适一些,我们可以将组件的一些行为转发给一个独立的策略对象,只需要替换相应的策略对象,就可以改变或者扩充组件的功能。

运用效果

  1. 比静态继承更灵活。与对象的静态继承相比,装饰模式提供了更加灵活的向对象添加职责的方式,可以在运行时刻增加和删除职责。相比之下,继承机制要求为每个添加的职责都创建一个新的子类,这样会产生很多新类,增加系统的复杂度。此外,我们可以提供不同的Decorator,这就使得我们可以对一些职责进行混合和匹配。
  2. 避免在层次结构高层的类有太多的特征。装饰模式提供了一种“即用即付”的方式来添加职责。这并不需要在一个复杂的可定制的类中支持所有可预见的特征,相反,我们完全可以具体的装饰类给装饰对象逐渐地添加各种功能,如此应用程序不必为不需要的功能而付出不必要的代价。
  3. Decorator与Component不一样。Decorator是一个透明的包装。如果我们从对象标识的观点出发,一个被装饰的了的组件与这个组件是有差别的,因此,使用装饰时不应该依赖对象标识。
  4. 会产生很多小对象。采用装饰模式进行系统设计往往会产生许多看一去类似的小对象,也就是各种具体的装饰类对象。新手学习这样的系统会相较困难,排错也不方便。

适用性

  1. 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。因为装饰器实现了组件接口,与具体的组件对象属于同一类型,故用户可以无区别地操作组件对象和装饰器对象。
  2. 处理那些可以撤销的职责。因为添加的职责只是额外职责,并非组件对象的核心职责,完全可以在不需要的时候进行撤销和替换它们。
  3. 当不能采用生成子类的方法进行扩充时。一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。另一种情况可能是因为类定义被隐藏,或类定义不能用于生成子类。

相关模式

  1. 装饰模式与组合模式:两者有相似之处,都涉及对象的递归调用,从某个角度来说,可以把装饰看作只有一个组件的组合。但是两者的目的是完全不一样的,装饰模式是要动态地透明地给对象增加功能,而组合模式是要管理基本对象和组合对象,为它们提供一个一致的接口给客户端,方便用户使用。
  2. 装饰模式与策略模式:策略模式亦可以实现动态地改变对象的功能,但是策略模式只是一层选择,也就是根据策略选择一下具体的实现类而已。而装饰模式不只一层,而是递归调用,无数层都可以,只要组合好装饰器的对象组合,就可以集资调用下去,所以装饰械更灵活些。当然,我们完全可以将两者组合在一起使用,在一个具体的装饰器中使用策略模式来选择更具体的实现方式。
  3. 装饰模式与模板方式:模式方法主要应用在算法骨架固定的情况,如果算法不固定就可以使用装饰模式,因为在使用装饰模式的时候,进行装饰器的组装时,其实就是一个调用算法步骤的组装,相当于一个动态的算法骨架。当然这只是模仿功能而已,两个设计模式的目的、功能和本质思想都是不一样的。这点大家需要明确。

总结

装饰模式的本质是:动态组合。动态是手段,组合才是目的。这里的组合有两层意义,一个是动态功能的组合,也就是动态进行装饰器的组合;另一个是指对象组合,通过对象组合来为被装饰对象透明地增加功能。此外装饰模式不仅可以增加功能,亦可以控制功能的访问,完全实现新的功能,同时也可以控制装饰的功能是在装饰功能之前还是之后立即来运行等。总之,装饰模式是通过把复杂功能简单化,分散化,然后在运行期间,根据需要动态组合相应的装饰器,获取相应的职责,这也是为什么需要将装饰器功能尽量细粒度化的原因,有利于复用。到这里,对装饰模式的讲解也已经接近尾声呢,下一篇,我们将继续学习另一种结构型模式——外观模式,敬请期待!

参考资料

  1. 程杰著《大话设计模式》一书
  2. 陈臣等著《研磨设计模式》一书
  3. GOF著《设计模式》一书
  4. Terrylee .Net设计模式系列文章
  5. 吕震宇老师 设计模式系列文章

TAG: