合成(Composite)模型模式属于对象的结构模式,有时又叫做部分-整体Part-Whole)模式。合成模式将对象组织到树结构中,可以用来描述整体与部分的关系。合成模式可以使客户端将单纯元素与复合元素同等看待。
对象的树结构
树结构在过程性的编程语言中曾今发挥了巨大的作用。在面向对象的语言中,树结构同样也威力巨大。一个基于继承的类型的等级结构便是一个数结构;一个基于合成的对象结构也是一个树结构。合成模式也是处理对象的树结构模式。
有向树结构的种类
有向树结构又可以分为三种:从上到下、从下到上和双向的。在三种有向树图中,树的节点和它们的相互关系都是一样的,但是连接它们的关系的方向却很不一样。
由上到下的树图
在由上向下的树图中,每一个树枝节点都有箭头指向它的所有的子节点,从而一个客户端可以要求一个树枝节点给出所有的子节点,而一个节点却并不知道它的父节点。在这样的树结构上,信息可以按照箭头所指的方向进行从上到下的传播。由上向下的树图如下图所示:
由下向上的树图
在一个由下向上的树图中,每一个节点都有箭头指向它们的父节点,但是一个父节点却不知道其子节点,信息可以按照箭头所指的方向自下向上传播。由下向上的树图如下图:
双向的树图
在一个双向的树图中,每一个节点都同时知道它的父节点和所有子节点。在这样的树结构上,信息可以按照箭头所指的方向向两个方向传播,双向树图如下图所示:
树图中的两种节点
一个书结构图由两种节点组成:树枝节点和树叶节点。树枝节点可以有子节点,而一个树叶节点不可能有子节点。一个树枝节点可以不带任何树叶节点,但是它因为有带有树叶节点的能力,因此仍然是树枝节点,而不会成为叶子节点,一个树叶节点则永远不可能带有子节点。
在信息系统里面,树枝节点所代表的构建常常用做树叶节点所代表的构建的容器。
根节点
一个数结构图中总有至少一个节点是特殊的节点,称作根节点。一个根节点没有父节点,因为它是树结构的根,一般讨论的树都是只有一个根节点的树。
合成模式
合成模式把部分和整体的关系用树结构表示出来。合成模式使得客户端把一个个单独的成分对象和由它它们复合而成的合成对象同等看待。
安全式和透明式的合成模式
下图所示的类图略去了各个角色的细节,没有给出它们各自的方法。
可以看出上面的类图结构涉及到三个角色:
- 抽象构件(Component)角色:这是一个抽象角色,它个参加组合的对象规定一个接口,这个角色给出共有的接口及其默认行文。
- 树叶构件(leaf)角色:代表参加组合的树叶对象。一个树叶没有下级的子对象。定义出参加组合的原始对象的行为。
- 树枝构件(Composite)角色:代表参加组合的有子对象的对象,并给出树枝构件对象的行为。
-
可以看出,Composite类型的对象含有其他的Component类型的对象。换而言之,Composite类型的对象可以含有其他树枝(Composite)类型或者树叶(Leaf)类型的对象。一个典型的树枝(Composite)对象如下图:
合成模式的实现根据所实现接口的区别分为两种形式,分别是安全是和透明式。
合成模式可以不提供父对象的管理方法,但是合成模式必须在合适的地方提供子对象的管理方法。
透明方式
作为第一种选择,在Component里面声明所有的用来管理子类对象的方法,包括add()、remove(),以及getChild()方法。这样做的好处是所有的构件类都有相同的接口。在客户端看了,树叶类对象与合成类对象的区别起码在接口层次上消失了,客户端可以同等地对待所有的对象,这就是透明形式的合成模式。
这个选择的缺点是不够安全,因为树叶类对象和合成类对象在本质上是有区别的。树叶类对象不可能有下一个层次的对象,因此add()、remove(),以及getChild()方法没有意义,在编译时不会出错,但在程序运行时会出错。
安全方式
作为第二种选择,在Composite里面声明所有用来管理子类对象的方法。这样的做法是安全的做法,因为树叶类型的对象根本就没有管理子类对象的方法,因此,如果客户端对树叶类对象使用这些方法时,程序会在编译的时候出错,这个选择的缺点是不够透明,因为树叶类和合成类将具有不同的接口。
安全式的合成模式结构
安全式的合成模式要求管理聚集的方法只出现在树枝构件类中,而不出现在树叶构件类中。安全式的合成模式的类图如下图:
这种形式涉及到三个角色: - 抽象构建(Component)角色:这个是一个抽象角色,它给参加组合的对象规定一个接口,规范共有的接口及默认行为。这个接口可以用来管理所有的子对象,要提供一个接口以规范取得和管理下层主键的接口,包括add()、remove()以及getchild()之类的方法。
- 树叶构件(leaf)角色:代表参加组合的树叶对象,定义出参加组合的原始对象的行为。树叶类会给出add()、remove()以及getchild()之类的用来管理子类对象的方法的平庸的实现。
- 树枝构件(Composite)角色:代表参加组合的有子对象的的对象,定义出这样的对象的行为。
源码如下:
Component.java 抽象构件角色
package com.model.composite.secure;
public interface Component {
/**
* 返回自己的实例
* */
Composite getComposite();
/**
* 某个商业方法
* */
void sampleOperation();
}
Composite.java 树枝构件角色
package com.model.composite.secure;
import java.util.Vector;
public class Composite implements Component{
private Vector<Component> componentVector = new Vector<Component>();
/**
* 返回自己的实例
* */
public Composite getComposite() {
return this;
}
/**
* 商业 业务逻辑方法
* */
public void sampleOperation() {
System.out.println("业务方法");
}
/**
* 聚集管理方法,增加一个子构件对象
* */
public void add(Component component){
componentVector.addElement(component);
}
/**
* 聚集管理方法,删除一个子构件对象
* */
public void remove(Component component){
componentVector.removeElement(component);
}
/**
* 聚集管理方法,返还聚集的对象
* */
public Vector<Component> components(){
return componentVector;
}
}
leaf.java 类 树叶构件角色
package com.model.composite.secure;
import java.util.Iterator;
import java.util.Vector;
public class Leaf implements Component{
/**
* 返还自己的实例
* */
public Composite getComposite() {
return null;
}
/**
* 商业 业务逻辑方法
* */
public void sampleOperation() {
}
/**
* 聚集管理方法,增加一个子构件对象
* */
public void add(Component component){
}
/**
* 聚集管理方法,删除一个子构件对象
* */
public void remove(Component component){
}
/**
* 聚集管理方法,返还聚集的对象
* */
public Vector<Component> components(){
return null;
}
}
树叶构件角色同意实现了抽象构件角色所声明的各种方法,但树叶构件角色没有子节点,因此没有聚集可以管理,所以它所给出的管理聚集的各种方法,包括add()、remove()以及components()等方法都是平庸的。
合成模式的实现
实现合成模式时,有几个可以考虑的问题:
(1)明显的给出父类对象的引用。在子对象里面给出父对象的引用,这可以很容易的遍历所有的父对象,管理合成结构。有了这个引用,可以方便地应用责任链模式。
定义出这个父对象引用的恰当地方就是Composite角色吗,树叶对象和合成对象都从这个父对象继承对父对象的引用。
在一个子对象被加到合成对象里面的时候,需要修改父对象的记录。当把子对象从合成对象里面删除时,也需要改变父对象的记录。因此需要在所有的构建对象里实现add()和remove()方法。调用add()方法将子对象加到合成对象上,调用remove()方法,将一个子对象从合成对象中删掉。
一般而言,一个对象持有对所有的子对象的引用,而没有对父对象的引用,当子对象持有对父对象的引用时,树结构就成为由下向上的树结构。当一个对象持有对所有子对象的引用时,树结构就成为由上向下的树结构。
如果一个对象既持有对其所有子对象的引用,又持有对其父对象的引用时,树结构就是双向的树结构。
(2)在通常的系统中,可以使用享元模式实现构件的共享,但是由于合成模式的对象经常要有对父类的引用,因此共享不容易实现。
(3)抽象构件类(Component)应当多“重”才算好。
一种思想是抽象构件类应该尽可能的“重”,选中这种形式的合成模式的目的便是使客户度不知道它所使用的是哪一个特殊的树叶构件或树枝构件。这意味着所有的具体构件类都有相同的接口,定义出这个接口的当然应当是抽象构件(Component)。
但是,如果抽象构件同时定义出树叶构件和树枝构件的接口,这便意味着有一些方法适用于树叶构件不适用于树枝构件;而另外一些方法适用于树枝构件而不适用于树叶构件。比如树枝构件有子构件,而树叶构件没有子构件。
另一种思想是使Component比较“轻”。因为管理子对象的这部分接口不适用于树叶部件,所以避免把合成部件特有的接口移动到抽象部件中去。这实际上就是安全式和透明式的合成模式。
(4)有时候系统需要遍历过一个树枝构件的子构件很多次,这时候就可以把遍历子构件的结果暂时存放在父构件里面,作为缓存。
(5)使用什么数据类型来存储对象。在给出的示意性代码里面,使用了Vector来存储合成对象所含有的子对象。但是在实际的系统里面不一定要使用Vector,也可以使用数组的其他剧集。
由于java语言的垃圾回收机制可以自动回收没有被引用的对象,这使得合成模式所涉及的对象可以在不被使用时自动被清除掉。在其他没有垃圾回收机制的语言中,需要考虑在什么地方设置对象删除的方法。
(6)Composite向子类的委派。客户端不应该直接调用树叶类,应当由其父类想树叶类进行委派,这样可以增加代码的重用性。