组合模式——结构型模式(3)

前言

今晚,是国庆长假的最后一晚呢,在这里,希望博友们都过上了一个愉快难忘的假期吧。明天大家就要开始全新的工作和学习呢,也希望大家今晚就能够转换好心态吧:)。好了,步入主题吧,上一篇,我们详细讲解了桥接模式,其主要目的是分离了抽象部分与实现部分,使它们各自可以沿着各自的维度进行扩展和变化,通过对象组合的方式使得抽象部分可以调用实现部分的操作,完成抽象部分与实现部分的桥接。接下来,让我们再次接受另外一种结构型模式的洗礼,来体会组合模式的内在之美吧!

动机

在实际的软件系统中,我们经常会面临一些简单对象元素和由这些简单对象元素组合而成的复杂对象元素的处理问题,如何提供一种封装机制,让客户端能够对两种类型进行无差别地操作,也就是能够使用户程序与复杂对象元素内部的复杂结构进行解耦。面对这样的一种需求,组合模式能够给我们提供较好的解决方案,马上进入对其的详细解说过程吧!

意图

将对象组合成树形结构以表示”部分——整体“的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。

要实现对简单对象和组合对象的一致性处理,关键点解在于设计一个抽象的组件类,让它可以代表组合对象和叶子对象。这样客户端就不用区分操作的是组合对象还是叶子对象(简单对象)呢,尽管将它们全部当作组件对象进行统一的操作即可。

结构图

image

  1. 抽象组件(component)角色:为组合中的对象声明接口。在适当的情况下,实现所有类共有接口的缺省行为,同时声明一个接口用于访问和管理Component的子组件,如果有必要的时候,可在递归结构中定义一个接口,用于访问一个父部件,并在合适的情况下实现它。
  2. 叶子组件(Leaf)角色:在组合中表示叶节点对象,叶节点没有子节点,也就是它是最底层对象,在组合对象中用于定义图元对象的行为。
  3. 树枝组件(Composite)角色:也可称为组合组件,就是包含若干个叶子组件的组件,其定义了有子组件的那些组件的行为,用于存储子组件,同时定义了自身组件的特有的行为操作。
  4. 客户(Client)角色:使用Component接口操纵组合组件的对象。

代码示例

 1:  public abstract class Component{
 2:      public abstract void Operation();
 3:      public void Add(Component component){}
 4:      public void Remove(Component component){}
 5:      public Component GetChild(int index){return null;}
 6:  }
 7:   
 8:  public class Leaf extends Component{
 9:      public void Operation(){
10:          System.out.println("Leaf is Operating!");
11:      }
12:  }
13:   
14:  public class Composite extends Component{
15:      private ArrayList<Component> components=null;
16:      public void Operation(){
17:          for (Component component : components) {
18:              component.Operation();
19:          }
20:      }
21:   
22:      public void Add(Component component){
23:          components.add(component);
24:      }
25:   
26:      public void Remove(Component component){
27:          components.remove(component);
28:      }
29:   
30:      public Component GetChild(int index){
31:          Component component=null;
32:          if(index>=0||index<components.size()){
33:              component=components.get(index);
34:          }
35:          return component;
36:      }
37:  }
38:   
39:  public class Client{
40:      public static void main(String[] args){
41:          Component leaf1=new Leaf();
42:          Component leaf2=new Leaf();
43:          Component composite=new Composite();
44:          composite.Add(leaf1);
45:          composite.Add(leaf2);
46:   
47:          composite.Operation();
48:   
49:          Component component=composite.GetChild(0);
50:          component.Operation();
51:      }
52:  }    

从上述简单的示意性代码中,我们可以基本上了解组合模式的内含与实现呢。很明显,我们通过一个抽象组件(Component)来定义所有不管是叶子组件(简单对象)还是组合组件(复杂对象)的所共有的接口,这样就为之后无差别地对待两者奠定了基础,因为统一了接口。Leaf与Composite都继承自父类Component,拥有共同的接口,但是对于不同类型的组件有不同的接口实现,比如组合组件的Operation操作事实上就是一个对所包含的所有子组件遍历一遍,同时调用各个子组件(或者说是简单对象)的Operation操作。与时同时,组合对象还必须实现存储和管理其包含的各个子组件,比如添加、删除、查找等操作。但是尽管它们之间的接口实现不一样,但是我们可以从客户端调用代码上看出,对这些不同组件的调用接口是完全一致的,在示例代码上体现为调用公共接口Operation。在这里需要注意的,组合模式有两种不同的实现方式,透明式和安全式,而我们的示例代码应该属于透明式的,因为不管是叶子组件还是组合组件都拥有完全相同的接口,尽管有些接口对叶子组件来说是不需要的,比如对子组件的管理接口,因为叶子组件本身不可能再含有子组件,它就是“最底层、最简单”的组件。至于透明式和安全式的组合模式实现的优缺点,下文会有更详细的论述。

现实场景

针对组合模式的实际情况,我们可以考虑这样一个具体的应用场景:在绘图编辑器和图形捕捉系统这样的图形应用程序中,用户可以使用简单的组件创建复杂的图表,用户可以组合多个简单组件以形成一些较大的组件,这些组件又可以组合成更大的组件。按照通常做法,我们可以定义一些像Text和Line这样的简单图元,再另外定义一些类作为这些图元的容器类,这样的做法看上去并没有不妥,但是当用户使用这些对象类时,由于接口的不同必须区别对待图元对象和容器对象,即使实际上大多数的情况用户认为它们应该是一致,具有相同接口的。这样就不利于这些图元对象的使用,让用户程序耦合度更高。现在让我们看看,组合模式是如何使用递归组合的方式使得用户不必对这些类进行差别对待,下面是其类结构设计图:

image

从类结构图中,我们可以很清楚地看到,现在不管是简单的图元对象(如Line、Rectangle和Text类)还是复杂的组合对象(也可以说是图元的容器类对象,应该Picture主要的作用还是用于组合/聚合各种图元对象,完成更复杂的组合对象,它即实现了抽象组件Graphic的所有接口,也提供了对其所包含的子组件的管理接口,也就是说它既是Graphic的子类,亦是所有图元的容器类,但是它的Draw接口的实现却是完全调用它所包含的所有图元对象的相应的Draw操作。我们可以通过上面的类结构图,设计得出由Graphic对象递归组合而成的组合对象结构:

image

最上层的Picture类不仅包含了普通的简单图元对象,同时也包含了复杂的组合对象(或者说容器类对象),而这个复杂组合对象本身又包含了若干个简单图元对象,这样就形成了一种典型的递归组合结构呢,这也是组合模式很容易实现的递归组合效果呢。接下来,我们来说说具体的示例代码如何实现的吧!

 1:  public abstract class Graphic{
 2:      public abstract void Draw();
 3:      public void Add(Graphic Graphic){}
 4:      public void Remove(Graphic Graphic){}
 5:      public Graphic GetChild(int index){return null;}
 6:  }
 7:   
 8:  public class Line extends Graphic{
 9:      public void Draw(){
10:          System.out.println("line is drawing!");
11:      }
12:  }
13:   
14:  public class Rectangle extends Graphic{
15:      public void Draw(){
16:          System.out.println("rectangle is drawing!");
17:      }
18:  }
19:   
20:  public class Text extends Graphic{
21:      public void Draw(){
22:          System.out.println("text is drawing!");
23:      }
24:  }
25:   
26:  public class Picture extends Graphic{
27:      private ArrayList<Graphic> Graphics=null;
28:      public void Draw(){
29:          System.out.println("picture is Drawing");
30:          for (Graphic Graphic : Graphics) {
31:              Graphic.Draw();
32:          }
33:          System.out.println("picture is Drawed");
34:      }
35:   
36:      public void Add(Graphic Graphic){
37:          Graphics.add(Graphic);
38:      }
39:   
40:      public void Remove(Graphic Graphic){
41:          Graphics.remove(Graphic);
42:      }
43:   
44:      public Graphic GetChild(int index){
45:          Graphic Graphic=null;
46:          if(index>=0||index<Graphics.size()){
47:              Graphic=Graphics.get(index);
48:          }
49:          return Graphic;
50:      }
51:  }
52:   
53:  public class Client{
54:      public static void main(String[] args){
55:          Graphic line1=new Line();
56:          Graphic text1=new Text();
57:          Graphic rectangle1=new Rectangle();
58:   
59:          Graphic child_picture=new Picture();
60:          child_picture.Add(line1);
61:          child_picture.Add(text1);
62:          child_picture.Add(rectangle1);
63:   
64:          Graphic line2=new Line();
65:          Graphic rectangle2=new Rectangle();
66:   
67:          Graphic picture=new Picture();
68:          picture.Add(child_picture);
69:          picture.Add(line2);
70:          picture.Add(rectangle2);
71:   
72:          //递归画出所有子组件
73:          picture.Draw();        
74:      }
75:  }

上述示意性代码已经将我们上文的意图演绎比较清楚、明白呢。代码73行处picture.Draw()操作将会递归地调用其所包含的所有图元对象,即使它也包含了同样为图元容器类的“图元”对象,之所以可以这样做,最关键的原因在于我们统一了图元与图元容器类的操作操作,这样使得我们可以无区别地对它们进行操作,而不需要进行事先的类型判别。

现在我们来谈谈组合模式的透明性和安全性的实现的差异。很明显,上面的示例代码就是典型的透明性实现方式,因为不管是简单的图元还是图元容器都拥有相同的操作接口,这样客户面对Graphic时就无需关心具体的组件类型呢。但是透明性的实现方式是以安全性为代价的,因为在Graphic中定义的一些方法,对图元对象来说是没有意义的,比如增加、删除和索引图元操作。但用户是不知道这些区别,对用户是透明的,因此用户可能会对图元对象调用这些对自身没有意义的操作,这样的操作自然就是不安全的。为了规避这样的不安全性问题,在透明性的实现方式中,我们通过会在Graphic类声明管理图元的操作,并为这些方式提供默认实现,如果图元不支持这些功能,默认的实现应该是抛出异常,告诉用户这并不支持这样的功能,但是对图元容器类来说,必须重写这些操作,实现真正对子组件的管理功能。

而安全性的实现方式就是将管理图元对象的操作定义在对应的图元容器类中,这样图元对象就不会发生使用添加或是删除图元的操作呢,因为它自身就没有提供这样的功能,这样的一种方式就是安全式的。那么相应的结构图应该是:

image

但是这样做的结果就是,用户在使用的时候,就必须区分使用的是图元对象(比如Line)还是图元容器对象(比如Picture),这样的判别操作自然对客户而言就变得不透明呢。针对安全性的实现代码这里就不在给出呢,有兴趣的童鞋可以自己实践一下,其实过程还是蛮简单的。

对于组合模式而言,在安全性和透明性上,通常会更看重透明性,毕竟组合模式的功能就是让用户对叶子对象和组合对象的使用上具有一致性。与此同时,安全性的实现方式未必就是完全安全的,当涉及类型转换时就有可能造成其他方面的不安全性。所以在日常使用组合模式的时候,建议多采用透明性的实现方式,而少采用安全性的实现方式,除非对安全性的要求达到一定层级时,我们才需要重点考虑安全式的实现方式。

实现要点

  1. 定义好叶子组件和组合组件都应该共同拥有的接口,这样使用它们时无需区别对待。
  2. 不管是叶子组件还是组合组件都应该实现抽象组件接口,保证两者可以无区别对待的前提基础;与此同时,组合组件还应该重写对子组件管理的所有接口,完成对子组件的管理功能。

运用效果

  1. 定义了包含基本对象和组合对象的类层次结构,基本对象可以被组合成复杂的组合对象,而组合对象亦可以组合成更复杂的组合对象,不断递归下去,从而构成一个统一的组合对象的类层次结构。
  2. 统一了组合对象和叶子对象,为它们两者定义了统一的父类,这样就将两者的行为统一了起来。
  3. 简化了客户端调用,因为组合模式通过统一组合对象和叶子对象,这样使得客户端使用它们时,无需区分它们,极大地简化了客户端的使用方式。
  4. 扩展性好,因为客户端统一地面对Component来操作,对新定义的组合对象和叶子对象很容易地融入已有的结构体系中来,客户端也不需要因为添加了新的组件类而改变。
  5. 运用组合模式难以限制组合中的组合类型,当需要检测组件类型的时候,我们不能依靠编译期的类型约束,必须在运行时刻动态检测。

适用性

  1. 想表示对象的部分——整体层次结构时。
  2. 希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合组合结构中的所有对象时。

相关模式

  1. 组合模式与装饰模式:两者可以组合使用。装饰模式在组装多个装饰器对象 的时候,是一个装饰器找下一下装饰器,依此递归下去。这种结构亦可以通过使用组合模式来构建,这样一来,装饰器对象就相当于组合模式中组合对象呢。当然此时,需要它们有一个公共父类,让装饰器支持组合模式中需要的一些对子组件的管理功能。
  2. 组合模式与享元模式:如果组合模式中出现大量相似的组件对象,并且组件对象的可变部分状态可以从组件对象中分离出去,同时组件对象不需要向父组件发送请求时,完全可以通过使用享元模式来帮助缓存复杂的组件对象,提高内存的复用率。
  3. 组合模式与迭代器模式:在组合模式中,可以通过使用迭代器模式来遍历组合对象中的子对象集合,而无需关心具体存放子对象的聚合结构。这点很实用。
  4. 组合模式与访问者模式:访问者模式可以在不修改原有对象结构的情况下,为对象结构中的对象增添新的功能。访问者模式与组合模式合用,可以把原本分散在组合对象和基本对象的操作和行为都局部化。
  5. 组合模式与职责链模式:职责链模式的意图是:将实现请求的发送者和接收者之间解耦,让多个接收者组合起来,构成职责链,然后将请求放于这条链上传递,直到有接收者处理这个请求来此。我们可以通过组合模式来构建这个职责链,也就是子组件找父组件,父组件又找父组件,如此递归下去(当然也可以是相反的过程),构成一条处理请求的组件对象链。
  6. 组合模式与命令模式:命令模式是将一个请求封装为一个对象,从而使你可以用不同的请求客户进行参数化工对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式中有一个宏命令(简单地说就是一个命令集)的功能,通常这个宏命令就是使用组合模式来组装生成而来的。

总结

组合模式的本原是:统一叶子对象和组合对象。正因为统一了两者,这样在将对象构建为树形结构时,才不需要进行区分对待,在操作树形结构时也变得简单,无需关注类型对象,统一操作。换句话来说,就是组合模式解耦了客户程序与复杂元素内部结构,从而使客户商可以像处理简单对象一样地处理复杂对象。其实针对组合模式,还有很多方面值得我们大家关注,比如父组件引用和环状引用问题,在较复杂的应用时,也是比较常遇到的,在这里我们就不在深入探讨呢,有兴趣的同学可以查阅《研磨设计模式》一书的相关章节,里面有很详细入微的讲解。接下来,在下一篇文章中,我们将详细讲解装饰器模式,敬请期待!

参考资料

  1. 程杰著《大话设计模式》一书
  2. 陈臣等著《研磨设计模式》一书
  3. GOF著《设计模式》一书
  4. Terrylee .Net设计模式系列文章
  5. 吕震宇老师 设计模式系列文章
posted @ 2012-10-07 20:45  JackyBing  阅读(1897)  评论(0编辑  收藏  举报