什么是组合模式
组合模式(Composite Pattern):组合多个对象形成树形结构以表示具有“整体—部分”关系的层次结构。组合模式对单个对象(即叶子对象)和组合对象(即容器对象)的使用具有一致性,组合模式又可以称为“整体—部分”(Part-Whole)模式,它是一种对象结构型模式。
组合模式将对象组织到树结构中,可以用来描述整体与部分的关系,可以使客户端将单纯元素与复合元素同等看待。
树结构在过程性的编程语言中曾经发挥了巨大的作用,在面向对象的语言中,树结构也同样威力巨大。一个基于继承的类型的等级结构便是一个树结构;一个基于组合的对象结构也是一个树结构。
在树形结构中,最顶层的节点被称为根节点,根节点下面可以包含树枝节点和叶子节点,树枝节点下面又可以包含树枝节点和叶子节点,如下图所示:
由上图可以看出,其实根节点和树枝节点本质上属于同一种数据类型,可以作为容器使用;而叶子节点与树枝节点在语义上不属于用一种类型。但是在组合模式中,会把树枝节点和叶子节点看作属于同一种数据类型(用统一接口定义),让它们具备一致行为。
这样,在组合模式中,整个树形结构中的对象都属于同一种类型,带来的好处就是用户不需要辨别是树枝节点还是叶子节点,可以直接进行操作,给用户的使用带来极大的便利。
模式的结构
组合模式UML类图
UML类图讲解:
Component(抽象构件):它可以是接口或抽象类,为叶子构件和容器构件对象声明接口,在该角色中可以包含所有子类共有行为的声明和实现。在抽象构件中定义了访问及管理它的子构件的方法,如增加子构件、删除子构件、获取子构件等。
Leaf(叶子构件):它在组合结构中表示叶子节点对象,叶子节点没有子节点,它实现了在抽象构件中定义的行为。对于那些访问及管理子构件的方法,可以通过异常等方式进行处理。
Composite(容器构件):它在组合结构中表示容器节点对象,容器节点包含子节点,其子节点可以是叶子节点,也可以是容器节点,它提供一个集合用于存储子节点,实现了在抽象构件中定义的行为,包括那些访问及管理子构件的方法,在其业务方法中可以递归调用其子节点的业务方法。
优点和缺点
优点
组合模式的主要优点如下:
组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制。
客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码。
在组合模式中增加新的容器构件和叶子构件都很方便,无须对现有类库进行任何修改,符合“开闭原则”。
组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子对象和容器对象的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单。
缺点
组合模式的主要缺点如下:
破坏了“单一职责原则”。
在增加新构件时很难对容器中的构件类型进行限制。有时候我们希望一个容器中只能有某些特定类型的对象,例如在某个文件夹中只能包含文本文件,使用组合模式时,不能依赖类型系统来施加这些约束,因为它们都来自于相同的抽象层,在这种情况下,必须通过在运行时进行类型检查来实现,这个实现过程较为复杂。
组合模式的实现根据所实现接口的区别分为透明式组合模式和安全式组合模式。
透明式
作为第一种选择,在Component里面声明所有的用来管理子类对象的方法,包括add()、remove(),以及 getChild()方法。这样做的好处是所有的构件类都有相同的接口。在客户端看来,树叶类对象与合成类对象的区别起码在接口层次上消失了,客户端可以同等地对待所有的对象。这就是透明形式的合成模式。
这个选择的缺点是不够安全,因为树叶类对象和合成类对象在本质上是有区别的。树叶类对象不可能有下一个层次的对象,因此add()、remove()以及 getChild()方法没有意义,但是在编译时期不会出错,而只会在运行时期才会出错。
透明式的组合模式要求所有的具体构件类,不论树枝构件还是树叶构件,都符合一个固定的接口,类图如下:
透明式组合模式涉及到抽象构件角色、树叶构件角色、树枝构件角色三种模式:
- 抽象构件(Component)角色:这是一个抽象角色,它给参加组合的对象规定一个接口,规范共有的接口及默认行为。这个接口可以用来管理所有的子对象,要提供一个接口以规范取得和管理下层组件的接口,包括 add()、remove()以及getChild()之类的方法。
- 树叶构件(Leaf〉角色:代表参加组合的树叶对象,定义出参加组合的原始对象的行为。树叶类会给出add()、remove()以及getChild()之类的用来管理子类对象的方法的平庸实现。
- 树枝构件(Composite)角色:代表参加组合的有子对象的对象,定义出这样的对象的行为。
我们都见过画图软件,一个绘图系统给出各种工具用来描绘线、长方形和原形等基本图形组成的图形。一个复杂的图形肯定是有这些基本的图形组成的。本模式我们就以这为例子来讲解。
由于一个复杂的图形是由基本图形组合而成的,因此,一个组合的图形应当有一个列表,存储对所有的基本图形对象的引用。复合图形的draw()方法在调用时,应当逐一调用所有列表上的基本图形对象的draw()方法。
透明形式的组合模式意味着不仅只有树枝构件角色才配备有管理聚集的方法,树叶构件也有这些方法,虽然树叶构件的这些方法是平庸的。透明式的组合模式的类图如下:
抽象构件角色:
树枝构件角色:
public class Picture extends Graphics {private Vector items = new Vector(10);//具体管理方法,增加一个子构件对象public void add(Graphics graphics){items.add(graphics);}//删除一个子构件对象public void remove(Graphics graphics){items.remove(graphics);}//返回一个子构件对象public .Graphics getChild(int i){return (Graphics) items.get(i);}@Overridepublic void draw() {for (int i = 0; i < items.size(); i++) {Graphics graphics = (Graphics) items.get(i);graphics.draw();}}
}
树叶构件角色:
package com.zeus;public class Line extends Graphics{@Overridevoid draw() {System.out.println("画了一条线");}@Overridevoid add() {}@Overridevoid remove() {}@OverrideGraphics getChild(int i) {return null;}
}package com.zeus;public class Circle extends Graphics{@Overridevoid draw() {System.out.println("画了一个圆形");}@Overridevoid add() {}@Overridevoid remove() {}@OverrideGraphics getChild(int i) {return null;}
}
package com.zeus;public class Rectangle extends Graphics{@Overridevoid draw() {System.out.println("画了一个长方形");}@Overridevoid add() {}@Overridevoid remove() {}@OverrideGraphics getChild(int i) {return null;}
}
测试:
打印的结果:
画了一个长方形
画了一条线
画了一个长方形
安全式
第二种选择是在 Composite类里面声明所有的用来管理子类对象的方法。这样的做法是安全的做法,因为树叶类型的对象根本就没有管理子类对象的方法,因此,如果客户端对树叶类对象使用这些方法时,程序会在编译时期出错。编译通不过,就不会出现运行时期错误。
这个选择的缺点是不够透明,因为树叶类和合成类将具有不同的接口。
安全式的组合模式要求管理具体的方法只出现在树枝构件类中,如下图所示:
安全式组合模式涉及到抽象构件角色、树叶构件角色、树枝构件角色这三个角色:
- 抽象构件角色(Component):这是一个抽象角色,他给参加组合的对象定义出公共的接口及其默认行为,可以用来管理所有的子对象。组合对象通常把它所包含的子对象当作类型为component的对象,在安全式的组合模式里,构件角色并不定义出管理子对象的方法
- 树叶构件角色(Leaf):树叶对象是没有下级子对象的对象,定义出参加组合的原始对象的行为
- 树枝构件角色(Composite):代表参加组合的有下级子对象的对象,树枝构件类给出所有的管理子对象的方法,如add(),remove()以及getChild()等方法
同样以上面的绘图系统为例子讲解安全式组合模式。安全式组合模式意味着只有树枝构件角色才能配备有管理聚集的方法,而树叶构件角色则没有这些方法。UML类图如下:
抽象构件角色:
树枝构件角色:
public class Picture extends Graphics{private Vector items = new Vector(10);//具体管理方法,增加一个子构件对象public void add(Graphics graphics){items.add(graphics);}//删除一个子构件对象public void remove(Graphics graphics){items.remove(graphics);}//返回一个子构件对象public Graphics getChild(int i){return (Graphics) items.get(i);}@Overridepublic void draw() {for (int i = 0; i < items.size(); i++) {Graphics graphics = (Graphics) items.get(i);graphics.draw();}}
}
树叶构件角色:
测试:
打印结果:
画了一个长方形。。。。
画了一条线。。。。
画了一个长方形。。。。
适用环境
在以下情况下可以考虑使用组合模式:
-
在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,客户端可以一致地对待它们。
-
在一个使用面向对象语言开发的系统中需要处理一个树形结构。
-
在一个系统中能够分离出叶子对象和容器对象,而且它们的类型不固定,需要增加一些新的类型。