结构型模式之组合模式

300 阅读12分钟

组合模式

定义: 将对象组合成树形结构以表示"部分-整体"的层次结构.组合模式使得用户对待单个对象或组合对象的使用具有一致性

对于树形结构,当容器对象(如文件夹)的某一个方法被调用时,将遍历整个树形结构,寻找也包含这个方法的成员对象(可以是容器对象,也可以是叶子对象)并调用执行,牵一而动百,其中使用了递归调用的机制来对整个结构进行处理。由于容器对象和叶子对象在功能上的区别,在使用这些对象的代码中必须有区别地对待容器对象和叶子对象,而实际上大多数情况下我们希望一致地处理它们,因为对于这些对象的区别对待将会使得程序非常复杂。组合模式为解决此类问题而诞生,它可以让叶子对象和容器对象的使用具有一致性

举个例子:文件夹目录结构,族谱,公司结构,货物管理结构等等

我们可以看出在第一张图中,包含文件和文件夹两类不同的元素,其中在文件夹中可以包含文件,还可以继续包含子文件夹,但是文件中不能再包含文件或者子文件夹.在此,我们可以称文件夹为容器(Container),而不同类型的各种文件是其成员,也称为叶子(Leaf),一个文件夹也可以作为另一个更大的文件夹的成员.如果我们需要对某一个文件夹进行操作,如查找文件,那么需要对指定的文件夹进行遍历,如果存在子文件夹则打开其子文件夹继续遍历,如果是文件则判断之后返回查找结果

模式结构和说明

在组合模式结构图中包含如下几个角色:

  • Component(抽象构件):它可以是接口或者抽象类,为叶子结构和容器结构对象声明接口,在该角色中可以包含所有子类共有的声明和实现.在抽象构件中定义了访问及管理它的子构件的方法,如增加子构件,删除子构件,获取子构件等
  • Composite(容器构件):它在组合结构中表示容器节点对象,容器节点包含子节点,其子节点可以是叶子节点,也可以是容器节点,它提供一个集合用于存储子节点,实现了在抽象构件中定义的行为,包含那些访问及管理子构件的方法,在其业务方法中可以递归调用其子节点的业务方法
  • Leaf(叶子构件):它在组合结构中表示叶子节点对象,叶子节点没有子节点,它实现了在抽象构件中定义的行为.对于那些访问及管理子构件的方法,可以通过异常等方式进行处理。

组合模式的关键是定义了一个抽象构建类,它既可以代表叶子,又可以代表容器,而客户端对该抽象构件类进行编程,无须知道它到底表示的是叶子还是容器,可以对其进行统一处理.容器对象中既可以包含叶子,也可以包含容器,以此实现递归组合,形成一个树形结构

下面通过简单的示例代码来分析组合模式的各个角色的用途和实现

简单示例说明

抽象构件角色:

abstract class Component {  
    public abstract void add(Component c); //增加成员  
    public abstract void remove(Component c); //删除成员  
    public abstract Component getChild(int i); //获取成员  
    public abstract void operation();  //业务方法  
} 

一般将抽象构件类设计为接口或者抽象类,将所有子类共有方法的声明和实现放在抽象构件类中.对于客户端而言,将针对抽象构件编程,而无须关心起具体子类是容器构件还是叶子构件

如果继承抽象构件的是叶子构件,代码如下:

class Leaf extends Component {
    public void add(Component c) { 
        //异常处理或错误提示 
    }   
    public void remove(Component c) { 
        //异常处理或错误提示 
    }
    public Component getChild(int i) { 
        //异常处理或错误提示
        return null; 
    }
    public void operation() {
        //叶子构件具体业务方法的实现
    } 
}

作为抽象构件类的子类,在叶子构件中需要实现在抽象构件类中声明的所有方法,包括业务方法以及管理和访问子构件的方法,但是叶子构件不能再包含子构件,因此在叶子构件中实现子构件管理和访问方法时需要提供异常处理或错误提示。

如果继承抽象构件的是容器构件,则其典型代码如下所示:

class Composite extends Component {
   private ArrayList<Component> list = new ArrayList<Component>();
   public void add(Component c) {
        list.add(c);
    }
   public void remove(Component c) {
        list.remove(c);
    }
   public Component getChild(int i) {
        return (Component)list.get(i);
    }
   public void operation() {
        //容器构件具体业务方法的实现
        //递归调用成员构件的业务方法
        for(Object obj:list) {
            ((Component)obj).operation();
        }
    }   
}

在容器构件中实现了在抽象构件中声明的所有方法,既包括业务方法,也包括用于访问和管理成员子构件的方法,如 add()、remove() 和 getChild() 等方法。需要注意的是在实现具体业务方法时,由于容器构件充当的是容器角色,包含成员构件,因此它将调用其成员构件的业务方法。在组合模式结构中,由于容器构件中仍然可以包含容器构件,因此在对容器构件进行处理时需要使用递归算法,即在容器构件的 operation() 方法中递归调用其成员构件的 operation() 方法。

模式讲解

认识组合模式

1.组合模式的目的

让客户端不再区分操作的是组合对象还是叶子对象,而是以一个统一的方式来操作.

**实现这个目标的关键之处,是设计一个抽象的组建类,让它可以代表组合对象和叶子对象.**这样一来客户端就不用区分到底是组合对象还是叶子对象了,只需要全部当成组件对象进行统一操作就可以了

2. 对象树

通常组合模式会组合出树形结构来,组成这个属性结构所使用的多个组件对象,就自然的形成了对象树

这也意味着凡是可以使用对象树来描述或操作的功能,都可以考虑使用组合模式,比如读取XML文件,或是对语句进行语法解析等。

3. 组合模式中的递归

组合模式中的递归,是对象本身的递归,是对象的组合方式,是从设计上来讲的,在设计上称作递归关联,是对象关联关系的一种

4. Component中是否应该实现一个Component列表

大多数情况下,一个Compoiste对象会持有子节点的集合.有些人会想,那么能不能把这个子节点集合定义到Component中去呢?因为在Component中还声明了一些操作子节点的方法,这样一来,大部分的工作就可以在Component中完成了。

事实上,这种方法是不太好的,因为在父类来存放子类的实例对象,对于Composite节点来说没有什么,它本来就需要存放子节点,但是对于叶子节点来说,就会导致空间的浪费,因为叶节点本身不需要子节点。

因此只有当组合结构中叶子对象数目较少的时候,才值得使用这种方法。

最大化Component定义

前面讲到了组合模式的目的是:让客户端不再区分操作的是组合对象还是叶子对象,而是以一种统一的方式来操作。

由于要统一两种对象的操作,所以Component里面的方法也主要是两种对象对外方法的和,换句话说,组件里面既有叶子对象需要的方法,也有组合对象需要的方法

其实这种实现是与类的设计原则相冲突的,类的设计有这样的原则:**一个父类应该只定义那些对它的子类有意义的操作。**但是看看上面的实现就知道,Component中的有些方法对于叶子对象是没有意义的。那么怎么解决这一冲突呢?

常见的做法是在Component里面为对某些子对象没有意义的方法,提供默认的实现,或是默认抛出不支持该功能的例外。这样一来,如果子对象需要这个功能,那就覆盖实现它,如果不需要,那就不用管了,使用父类的默认实现就可以了。

从另一个层面来说,如果把叶子对象看成是一个特殊的Composite对象,也就是没有子节点的组合对象而已。这样看来,对于Component而言,子对象就全部看作是组合对象,因此定义的所有方法都是有意义的了。

安全性和透明性

根据前面的讲述,在组合模式中,把组件对象分成了两种,一种是可以包含子组件的Composite对象,一种是不能包含其它组件对象的叶子对象。

Composite对象就像是一个容器,可以包含其它的Composite对象或叶子对象。当然有了容器,就要能对这个容器进行维护,需要向里面添加对象,并能够从容器里面获取对象,还有能从容器中删除对象,也就是说需要管理子组件对象。

这就产生了一个很重要的问题:到底在组合模式的类层次结构中,在哪一些类里面定义这些管理子组件的操作,到底应该在Component中声明这些操作,还是在Composite中声明这些操作?

这就需要仔细思考,在不同的实现中,进行安全性和透明性的权衡选择。

这里所说的安全性是指:从客户使用组合模式上看是否更安全。如果是安全的,那么不会有发生误操作的可能,能访问的方法都是被支持的功能。

这里所说的透明性是指:从客户使用组合模式上,是否需要区分到底是组合对象还是叶子对象。如果是透明的,那就是不再区分,对于客户而言,都是组件对象,具体的类型对于客户而言是透明的,是客户无需要关心的。

1. 透明性的实现

如果把管理子组件的操作定义在Component中,那么客户端只需要面对Component,而无需关心具体的组件类型,这种实现方式就是透明性的实现。

但是透明性的实现是以安全性为代价的,因为在Component中定义的一些方法,对于叶子对象来说是没有意义的,比如:增加、删除子组件对象。而客户不知道这些区别,对客户是透明的,因此客户可能会对叶子对象调用这种增加或删除子组件的方法,这样的操作是不安全的。

组合模式的透明性实现,通常的方式是:在Component中声明管理子组件的操作,并在Component中为这些方法提供缺省的实现,如果是有子对象不支持的功能,缺省的实现可以是抛出一个例外,来表示不支持这个功能

2. 安全性的实现

如果把管理子组件的操作定义在Composite中,那么客户在使用叶子对象的时候,就不会发生使用添加子组件或是删除子组件的操作了,因为压根就没有这样的功能,这种实现方式是安全的。

但是这样一来,客户端在使用的时候,就必须区分到底使用的是Composite对象,还是叶子对象,不同对象的功能是不一样的。也就是说,这种实现方式,对客户而言就不是透明的了

3. 两种实现方式的选择

对于组合模式而言,在安全性和透明性上,会更看重透明性,毕竟组合模式的功能就是要让用户对叶子对象和组合对象的使用具有一致性

而且对于安全性的实现,需要区分是组合对象还是叶子对象,但是有的时候,你需要将对象进行类型转换,却发现类型信息丢失了,只好强行转换,这种类型转换必然是不够安全的

对于这种情况的处理方法是在Component里面定义一个getComposite的方法,用来判断是组合对象还是叶子对象,如果是组合对象,就返回组合对象,如果是叶子对象,就返回null,这样就可以先判断,然后再强制转换。因此在使用组合模式的时候,建议多用透明性的实现方式,而少用安全性的实现方式