组合模式

Author Avatar
stormjie 10月 07, 2018
  • 在其它设备中阅读本文章

一、组合模式概述

树形结构在软件中随处可见,例如操作系统中的目录结构、应用软件中的菜单、办公系统中的公司组织结构等等,如何运用面向对象的方式来处理这种树形结构是组合模式需要解决的问题,组合模式通过一种巧妙的设计方案使得用户可以一致性地处理整个树形结构或者树形结构的一部分,也可以一致性地处理树形结构中的叶子节点(不包含子节点的节点)和容器节点(包含子节点的节点)。

比如一个文件系统就是一个典型的组合模式系统。下图是常见的计算机XP文件系统的一部分。

从上图可以看出,文件系统是一个树结构,树上长有节点。树的节点有两种,一种是树枝节点,即目录,有内部树结构,在图中涂有颜色;另一种是文件,即树叶节点,没有内部树结构。

显然,可以把目录和文件当做同一种对象同等对待和处理,这也就是组合模式的应用。

组合模式可以不提供父对象的管理方法,但是组合模式必须在合适的地方提供子对象的管理方法,诸如:add()、remove()、以及getChild()等。

组合模式定义如下:

组合模式(Composite Pattern):组合多个对象形成树形结构以表示具有“整体—部分”关系的层次结构。组合模式对单个对象(即叶子对象)和组合对象(即容器对象)的使用具有一致性,组合模式又可以称为“整体—部分”(Part-Whole)模式,它是一种对象结构型模式。

组合模式的实现根据所实现接口的区别分为两种形式,分别称为安全式透明式

二、安全式组合模式的结构

安全模式的组合模式要求管理聚集的方法只出现在树枝构件类中,而不出现在树叶构件类中。

这种形式涉及到三个角色:

  • 抽象构件(Component)角色:这是一个抽象角色,它给参加组合的对象定义出公共的接口及其默认行为,可以用来管理所有的子对象。合成对象通常把它所包含的子对象当做类型为Component的对象。在安全式的组合模式里,构件角色并不定义出管理子对象的方法,这一定义由树枝构件对象给出。

  • 树叶构件(Leaf)角色:树叶对象是没有下级子对象的对象,定义出参加组合的原始对象的行为。

  • 树枝构件(Composite)角色:代表参加组合的有下级子对象的对象。树枝构件类给出所有的管理子对象的方法,如add()、remove()以及getChild()。

以下是示例代码:

//抽象构件角色类
public interface Component {

    // 输出组建自身的名称
    public void printStruct(String preStr);
}
//树枝构件角色类
public class Composite implements Component {

    //用来存储组合对象中包含的子组件对象
    private List<Component> childComponents = new ArrayList<Component>();

    //组合对象的名字
    private String name;

    //构造方法,传入组合对象的名字
    public Composite(String name) {
        this.name = name;
    }

    //聚集管理方法,增加一个子构件对象
    public void addChild(Component child) {
        childComponents.add(child);
    }

    //聚集管理方法,删除一个子构件对象
    public void removeChild(int index) {
        childComponents.remove(index);
    }

    //聚集管理方法,返回所有子构件对象
    public List<Component> getChild() {
        return childComponents;
    }

    //输出对象的自身结构
    @Override
    public void printStruct(String preStr) {
        // 先把自己输出
        System.out.println(preStr + "+" + this.name);
        //如果还包含有子组件,那么就输出这些子组件对象
        if(this.childComponents != null) {
            //添加两个空格,表示向后缩进两个空格
            preStr += "  ";
            //输出当前对象的子对象
            for(Component c : childComponents) {
                //递归输出每个子对象
                c.printStruct(preStr);
            }
        }        
    }

}
//树叶构件角色类
public class Leaf implements Component {

    //叶子对象的名字
    private String name;

    //构造方法,传入叶子对象的名称
    public Leaf(String name) {
        this.name = name;
    }

    //输出叶子对象的结构,叶子对象没有子对象,也就是输出叶子对象的名字
    @Override
    public void printStruct(String preStr) {
        // TODO Auto-generated method stub
        System.out.println(preStr + "-" + name);
    }

}
//客户端类
public class Client {
    public static void main(String[]args) {
        Composite root = new Composite("服装");
        Composite c1 = new Composite("男装");
        Composite c2 = new Composite("女装");

        Leaf leaf1 = new Leaf("衬衫");
        Leaf leaf2 = new Leaf("夹克");
        Leaf leaf3 = new Leaf("裙子");
        Leaf leaf4 = new Leaf("套装");

        root.addChild(c1);
        root.addChild(c2);
        c1.addChild(leaf1);
        c1.addChild(leaf2);
        c2.addChild(leaf3);
        c2.addChild(leaf4);

        root.printStruct("");
    }
}

可以看出,树枝构件类(Composite)给出了addChild()、removeChild()以及getChild()等方法的声明和实现,而树叶构件类则没有给出这些方法的声明或实现。这样的做法是安全的做法,由于这个特点,客户端应用程序不可能错误地调用树叶构件的聚集方法,因为树叶构件没有这些方法,调用会导致编译错误。

安全式组合模式的缺点是不够透明,因为树叶类和树枝类将具有不同的接口。

三、透明式组合模式的结构

与安全式的组合模式不同的是,透明式的组合模式要求所有的具体构件类,不论树枝构件还是树叶构件,均符合一个固定接口。

以下是示例代码:

//抽象构件角色类
public abstract class Component {

    //输出组建自身的名称
    public abstract void printStruct(String preStr);

    //聚集管理方法,增加一个子构件对象
    public void addChild(Component child) {
        /**
         * 缺省实现,抛出异常,因为叶子对象没有此功能
         * 或者子组件没有实现这个功能
         */
        throw new UnsupportedOperationException("对象不支持此功能");
    }

    //聚集管理方法,删除一个子构件对象
    public void removeChild(int index) {
        /**
         * 缺省实现,抛出异常,因为叶子对象没有此功能
         * 或者子组件没有实现这个功能
         */
        throw new UnsupportedOperationException("对象不支持此功能");
    }

    //聚集管理方法,返回所有子构件对象
    public List<Component> getChild() {
        /**
         * 缺省实现,抛出异常,因为叶子对象没有此功能
         * 或者子组件没有实现这个功能
         */
        throw new UnsupportedOperationException("对象不支持此功能");
    }
}
//树枝构件角色类,此类将implements Conponent改为extends Conponent,其他地方无变化。
public class Composite extends Component {

    //用来存储组合对象中包含的子组件对象
    private List<Component> childComponents = new ArrayList<Component>();

    //组合对象的名字
    private String name;

    //构造方法,传入组合对象的名字
    public Composite(String name) {
        this.name = name;
    }

    //聚集管理方法,增加一个子构件对象
    public void addChild(Component child) {
        childComponents.add(child);
    }

    //聚集管理方法,删除一个子构件对象
    public void removeChild(int index) {
        childComponents.remove(index);
    }

    //聚集管理方法,返回所有子构件对象
    public List<Component> getChild() {
        return childComponents;
    }

    //输出对象的自身结构
    @Override
    public void printStruct(String preStr) {
        // 先把自己输出
        System.out.println(preStr + "+" + this.name);
        //如果还包含有子组件,那么就输出这些子组件对象
        if(this.childComponents != null) {
            //添加两个空格,表示向后缩进两个空格
            preStr += "  ";
            //输出当前对象的子对象
            for(Component c : childComponents) {
                //递归输出每个子对象
                c.printStruct(preStr);
            }
        }        
    }

}
//树叶构件角色类,此类将implements Conponent改为extends Conponent,其他地方无变化。
public class Leaf extends Component {

    //叶子对象的名字
    private String name;

    //构造方法,传入叶子对象的名称
    public Leaf(String name) {
        this.name = name;
    }

    //输出叶子对象的结构,叶子对象没有子对象,也就是输出叶子对象的名字
    @Override
    public void printStruct(String preStr) {
        // TODO Auto-generated method stub
        System.out.println(preStr + "-" + name);
    }

}
//客户端类的主要变化是不再区分Composite对象和Leaf对象。
public class Client {
    public static void main(String[]args) {
        Component root = new Composite("服装");
        Component c1 = new Composite("男装");
        Component c2 = new Composite("女装");

        Component leaf1 = new Leaf("衬衫");
        Component leaf2 = new Leaf("夹克");
        Component leaf3 = new Leaf("裙子");
        Component leaf4 = new Leaf("套装");

        root.addChild(c1);
        root.addChild(c2);
        c1.addChild(leaf1);
        c1.addChild(leaf2);
        c2.addChild(leaf3);
        c2.addChild(leaf4);

        root.printStruct("");
    }
}

可以看出,客户端无需再区分操作的是树枝对象(Composite)还是树叶对象(Leaf)了;对于客户端而言,操作的都是Component对象。

四、两种实现方法的选择

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

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

对于组合模式而言,在安全性和透明性上,会更看重透明性,毕竟组合模式的目的是:让客户端不再区分操作的是树枝对象还是树叶对象,而是以一个统一的方式来操作。

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

因此在使用组合模式的时候,建议多采用透明性的实现方式。

五、组合模式总结

组合模式使用面向对象的思想来实现树形结构的构建与处理,描述了如何将容器对象和叶子对象进行递归组合,实现简单,灵活性好。由于在软件开发中存在大量的树形结构,因此组合模式是一种使用频率较高的结构型设计模式,Java SE中的AWT和Swing包的设计就基于组合模式,在这些界面包中为用户提供了大量的容器构件(如Container)和成员构件(如Checkbox、Button和TextComponent等),其结构如图:

在图中,Component类是抽象构件,Checkbox、Button和TextComponent是叶子构件,而Container是容器构件,在AWT中包含的叶子构件还有很多,因为篇幅限制没有在图中一一列出。在一个容器构件中可以包含叶子构件,也可以继续包含容器构件,这些叶子构件和容器构件一起组成了复杂的GUI界面。

除此以外,在XML解析、组织结构树处理、文件系统设计等领域,组合模式都得到了广泛应用。

1. 主要优点

组合模式的主要优点如下:

(1)组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制。

(2)客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码。

(3)在组合模式中增加新的容器构件和叶子构件都很方便,无须对现有类库进行任何修改,符合“开闭原则”。

(4)组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子对象和容器对象的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单。

2.主要缺点

组合模式的主要缺点如下:

在增加新构件时很难对容器中的构件类型进行限制。有时候我们希望一个容器中只能有某些特定类型的对象,例如在某个文件夹中只能包含文本文件,使用组合模式时,不能依赖类型系统来施加这些约束,因为它们都来自于相同的抽象层,在这种情况下,必须通过在运行时进行类型检查来实现,这个实现过程较为复杂。

3.适用场景

在以下情况下可以考虑使用组合模式:

(1)在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,客户端可以一致地对待它们。

(2)在一个使用面向对象语言开发的系统中需要处理一个树形结构。

(3)在一个系统中能够分离出叶子对象和容器对象,而且它们的类型不固定,需要增加一些新的类型。