摘要
JDK:1.8.0_202
# 一:场景问题
# 1.1 商品类别树
考虑这样一个实际的应用:管理商品类别树。
在实现跟商品有关的应用系统的时候,一个很常见的功能就是商品类别树的管理,比如有如下所示的商品类别树:
- 服装
- 男装
- 衬衣
- 夹克
- 女装
- 裙子
- 套装
2
3
4
5
6
7
仔细观察上面的商品类别树,有以下几个明显的特点:
有一个根节点,比如服装,它没有父节点,它可以包含其它的节点;
树枝节点,有一类节点可以包含其它的节点,称之为树枝节点,比如男装、女装;
叶子节点,有一类节点没有子节点,称之为叶子节点,比如衬衣、夹克、裙子、套装;
现在需要管理商品类别树,假如就要求能实现输出如上商品类别树的结构的功能,应该如何实现呢?
# 1.2 不用模式的解决方案
要管理商品类别树,就是要管理树的各个节点,现在树上的节点有三类,根节点、树枝节点和叶子节点,再进一步分析发现,根节点和树枝节点是类似的,都是可以包含其它节点的节点,把它们称为容器节点。
这样一来,商品类别树的节点就被分成了两种,一种是容器节点,另一种是叶子节点。容器节点可以包含其它的容器节点或者叶子节点。把它们分别实现成为对象,也就是容器对象和叶子对象,容器对象可以包含其它的容器对象或者叶子对象,换句话说,容器对象是一种组合对象。
然后在组合对象和叶子对象里面去实现要求的功能就可以了,看看代码实现。
叶子对象代码实现:
/**
* 叶子对象
*/
public class Leaf {
/**
* 叶子对象的名字
*/
private String name = "";
/**
* 构造方法,传入叶子对象的名字
*
* @param name 叶子对象的名字
*/
public Leaf(String name) {
this.name = name;
}
/**
* 输出叶子对象的结构,叶子对象没有子对象,也就是输出叶子对象的名字
*
* @param preStr 前缀,主要是按照层级拼接的空格,实现向后缩进
*/
public void printStruct(String preStr) {
System.out.println(preStr + "-" + name);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
组合对象的代码实现,组合对象里面可以包含其它的组合对象或者是叶子对象,由于类型不一样,需要分开记录:
/**
* 组合对象,可以包含其它组合对象或者叶子对象
*/
public class Composite {
/**
* 用来记录包含的其它组合对象
*/
private Collection<Composite> childComposite = new ArrayList<Composite>();
/**
* 用来记录包含的其它叶子对象
*/
private Collection<Leaf> childLeaf = new ArrayList<Leaf>();
/**
* 组合对象的名字
*/
private String name = "";
/**
* 构造方法,传入组合对象的名字
*
* @param name 组合对象的名字
*/
public Composite(String name) {
this.name = name;
}
/**
* 向组合对象加入被它包含的其它组合对象
*
* @param c 被它包含的其它组合对象
*/
public void addComposite(Composite c) {
this.childComposite.add(c);
}
/**
* 向组合对象加入被它包含的叶子对象
*
* @param leaf 被它包含的叶子对象
*/
public void addLeaf(Leaf leaf) {
this.childLeaf.add(leaf);
}
/**
* 输出组合对象自身的结构
*
* @param preStr 前缀,主要是按照层级拼接的空格,实现向后缩进
*/
public void printStruct(String preStr) {
//先把自己输出去
System.out.println(preStr + "+" + this.name);
//然后添加一个空格,表示向后缩进一个空格,输出自己包含的叶子对象
preStr += " ";
for (Leaf leaf : childLeaf) {
leaf.printStruct(preStr);
}
//输出当前对象的子对象了
for (Composite c : childComposite) {
//递归输出每个子对象
c.printStruct(preStr);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
客户端:
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.addComposite(c1);
root.addComposite(c2);
c1.addLeaf(leaf1);
c1.addLeaf(leaf2);
c2.addLeaf(leaf3);
c2.addLeaf(leaf4);
//调用根对象的输出功能来输出整棵树
root.printStruct("");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 1.3 有何问题
上面的实现,虽然能实现要求的功能,但是有一个很明显的问题:那就是必须区分组合对象和叶子对象,并进行有区别的对待,比如在Composite和Client里面,都需要去区别对待这两种对象。
区别对待组合对象和叶子对象,不仅让程序变得复杂,还对功能的扩展也带来不便。实际上,大多数情况下用户并不想要去区别它们,而是认为它们是一样的,这样他们操作起来最简单。
换句话说,对于这种具有整体与部分关系,并能组合成树形结构的对象结构,如何才能够以一个统一的方式来进行操作呢?
# 二:解决方案
用来解决上述问题的一个合理的解决方案就是组合模式。
组合模式:将对象组合成树形结构以表示 "部分-整体" 的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
# 2.1 解决思路
仔细分析上面不用模式的例子中,要区分组合对象和叶子对象的根本原因,就在于没有把组合对象和叶子对象统一起来,也就是说,组合对象类型和叶子对象类型是完全不同的类型,这导致了操作的时候必须区分它们。
组合模式通过引入一个抽象的组件对象,作为组合对象和叶子对象的父对象,这样就把组合对象和叶子对象统一起来了,用户使用的时候,始终是在操作组件对象,而不再去区分是在操作组合对象还是在操作叶子对象。
组合模式的关键就在于这个抽象类,这个抽象类既可以代表叶子对象,也可以代表组合对象,这样用户在操作的时候,对单个对象和组合对象的使用就具有了一致性。
# 2.2 模式结构和说明
组合模式的结构如图所示:
- Component:抽象的组件对象,为组合中的对象声明接口,让客户端可以通过这个接口来访问和管理整个对象结构,可以在里面为定义的功能提供缺省的实现。
- Leaf:叶子节点对象,定义和实现叶子对象的行为,不再包含其它的子节点对象。
- Composite:组合对象,通常会存储子组件,定义包含子组件的那些组件的行为,并实现在组件接口中定义的与子组件有关的操作。
- Client:客户端,通过组件接口来操作组合结构里面的组件对象。
# 2.3 示例代码
组合对象的定义,示例代码如下:
/**
* 抽象的组件对象,为组合中的对象声明接口,实现接口的缺省行为
*/
public abstract class Component {
/**
* 示意方法,子组件对象可能有的功能方法
*/
public abstract void someOperation();
/**
* 向组合对象中加入组件对象
*
* @param child 被加入组合对象中的组件对象
*/
public void addChild(Component child) {
// 缺省的实现,抛出例外,因为叶子对象没有这个功能,
// 或者子组件没有实现这个功能
throw new UnsupportedOperationException("对象不支持这个功能");
}
/**
* 从组合对象中移出某个组件对象
*
* @param child 被移出的组件对象
*/
public void removeChild(Component child) {
// 缺省的实现,抛出例外,因为叶子对象没有这个功能,
// 或者子组件没有实现这个功能
throw new UnsupportedOperationException("对象不支持这个功能");
}
/**
* 返回某个索引对应的组件对象
*
* @param index 需要获取的组件对象的索引,索引从0开始
* @return 索引对应的组件对象
*/
public Component getChildren(int index) {
// 缺省的实现,抛出例外,因为叶子对象没有这个功能,
// 或者子组件没有实现这个功能
throw new UnsupportedOperationException("对象不支持这个功能");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
Composite的对象,示例代码如下:
/**
* 组合对象,通常需要存储子对象,定义有子部件的部件行为,
* 并实现在Component里面定义的与子部件有关的操作
*/
public class Composite extends Component {
/**
* 用来存储组合对象中包含的子组件对象
*/
private List<Component> childComponents = null;
/**
* 示意方法,通常在里面需要实现递归的调用
*/
@Override
public void someOperation() {
if (childComponents != null) {
for (Component c : childComponents) {
// 递归的进行子组件相应方法的调用
c.someOperation();
}
}
}
@Override
public void addChild(Component child) {
// 延迟初始化
if (childComponents == null) {
childComponents = new ArrayList<>();
}
childComponents.add(child);
}
@Override
public void removeChild(Component child) {
if (childComponents != null) {
childComponents.remove(child);
}
}
@Override
public Component getChildren(int index) {
if (childComponents != null) {
if (index >= 0 && index < childComponents.size()) {
return childComponents.get(index);
}
}
return null;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
叶子对象的定义,示例代码如下:
/**
* 叶子对象,叶子对象不再包含其它子对象
*/
public class Leaf extends Component {
/**
* 示意方法,叶子对象可能有自己的功能方法
*/
public void someOperation() {
// do something
}
}
2
3
4
5
6
7
8
9
10
11
客户端:
public class Client {
public static void main(String[] args) {
//定义多个Composite对象
Component root = new Composite();
Component c1 = new Composite();
Component c2 = new Composite();
//定义多个叶子对象
Component leaf1 = new Leaf();
Component leaf2 = new Leaf();
Component leaf3 = new Leaf();
//组合成为树形的对象结构
root.addChild(c1);
root.addChild(c2);
root.addChild(leaf1);
c1.addChild(leaf2);
c2.addChild(leaf3);
//操作Component对象
Component o = root.getChildren(1);
o.someOperation();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 2.4 重写案例
结构如下图所示:
为组合对象和叶子对象添加一个抽象的父对象做为组件对象,在组件对象里面,定义一个输出组件本身名称的方法以实现要求的功能,示例代码如下:
/**
* 抽象的组件对象
*/
public abstract class Component {
/**
* 输出组件自身的名称
*/
public abstract void printStruct(String preStr);
/**
* 向组合对象中加入组件对象
*
* @param child 被加入组合对象中的组件对象
*/
public void addChild(Component child) {
throw new UnsupportedOperationException("对象不支持这个功能");
}
/**
* 从组合对象中移出某个组件对象
*
* @param child 被移出的组件对象
*/
public void removeChild(Component child) {
throw new UnsupportedOperationException("对象不支持这个功能");
}
/**
* 返回某个索引对应的组件对象
*
* @param index 需要获取的组件对象的索引,索引从0开始
* @return 索引对应的组件对象
*/
public Component getChildren(int index) {
throw new UnsupportedOperationException("对象不支持这个功能");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
叶子节点,示例代码如下:
/**
* 叶子对象
*/
public class Leaf extends Component {
/**
* 叶子对象的名字
*/
private String name = "";
/**
* 构造方法,传入叶子对象的名字
*
* @param name 叶子对象的名字
*/
public Leaf(String name) {
this.name = name;
}
/**
* 输出叶子对象的结构,叶子对象没有子对象,也就是输出叶子对象的名字
*
* @param preStr 前缀,主要是按照层级拼接的空格,实现向后缩进
*/
public void printStruct(String preStr) {
System.out.println(preStr + "-" + name);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
组合对象的实现,示例代码如下:
/**
* 组合对象,可以包含其它组合对象或者叶子对象
*/
public class Composite extends Component {
/**
* 用来存储组合对象中包含的子组件对象
*/
private List<Component> childComponents = null;
/**
* 组合对象的名字
*/
private String name = "";
/**
* 构造方法,传入组合对象的名字
*
* @param name 组合对象的名字
*/
public Composite(String name) {
this.name = name;
}
public void addChild(Component child) {
//延迟初始化
if (childComponents == null) {
childComponents = new ArrayList<>();
}
childComponents.add(child);
}
/**
* 输出组合对象自身的结构
*
* @param preStr 前缀,主要是按照层级拼接的空格,实现向后缩进
*/
public void printStruct(String preStr) {
//先把自己输出去
System.out.println(preStr + "+" + this.name);
//如果还包含有子组件,那么就输出这些子组件对象
if (this.childComponents != null) {
//然后添加一个空格,表示向后缩进一个空格
preStr += " ";
//输出当前对象的子对象了
for (Component c : childComponents) {
//递归输出每个子对象
c.printStruct(preStr);
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
客户端:
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("");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
运行结果:
通过使用组合模式,把一个“部分-整体”的层次结构表示成了对象树的结构,这样一来,客户端就无需再区分操作的是组合对象还是叶子对象了,对于客户端而言,操作的都是组件对象。
# 三:模式讲解
# 3.1 认识组合模式
1. 组合模式的目的
组合模式的目的是:让客户端不再区分操作的是组合对象还是叶子对象,而是以一个统一的方式来操作。
实现这个目标的关键之处,是设计一个抽象的组件类,让它可以代表组合对象和叶子对象。这样一来,客户端就不用区分到底是组合对象还是叶子对象了,只需要全部当成组件对象进行统一的操作就可以了。
2. 对象树
通常,组合模式会组合出树形结构来,组成这个树形结构所使用的多个组件对象,就自然的形成了对象树。
这也意味着凡是可以使用对象树来描述或操作的功能,都可以考虑使用组合模式,比如读取XML文件,或是对语句进行语法解析等。
3. Component中是否应该实现一个Component列表
大多数情况下,一个Composite对象会持有子节点的集合。有些朋友可能就会想,那么能不能把这个子节点集合定义到Component中去呢?因为在Component中还声明了一些操作子节点的方法,这样一来,大部分的工作就可以在Component中完成了。
事实上,这种方法是不太好的,因为在父类来存放子类的实例对象,对于Composite节点来说没有什么,它本来就需要存放子节点,但是对于叶子节点来说,就会导致空间的浪费,因为叶节点本身不需要子节点。
因此只有当组合结构中叶子对象数目较少的时候,才值得使用这种方法。
4. 最大化Component定义
前面讲到了组合模式的目的是:让客户端不再区分操作的是组合对象还是叶子对象,而是以一种统一的方式来操作。
由于要统一两种对象的操作,所以Component里面的方法也主要是两种对象对外方法的和,换句话说,有点大杂烩的意思,组件里面既有叶子对象需要的方法,也有组合对象需要的方法。
其实这种实现是与类的设计原则相冲突的,类的设计有这样的原则:一个父类应该只定义那些对它的子类有意义的操作。但是看看上面的实现就知道,Component中的有些方法对于叶子对象是没有意义的。那么怎么解决这一冲突呢?
常见的做法是在Component里面为对某些子对象没有意义的方法,提供默认的实现,或是默认抛出不支持该功能的例外。这样一来,如果子对象需要这个功能,那就覆盖实现它,如果不需要,那就不用管了,使用父类的默认实现就可以了。
从另一个层面来说,如果把叶子对象看成是一个特殊的Composite对象,也就是没有子节点的组合对象而已。这样看来,对于Component而言,子对象就全部看作是组合对象,因此定义的所有方法都是有意义的了。
5. 子部件排序
在某些应用中,使用组合模式的时候,需要按照一定的顺序来使用子组件对象,比如进行语法分析的时候,使用组合模式构建的抽象语法树,在解析执行的时候,是需要按照顺序来执行的。
对于这样的功能,需要在设计的时候,就要把组件对象的索引考虑进去,并仔细的设计对子节点的访问和管理接口,通常的方式是需要按照顺序来存储,这样在获取的时候就可以按照顺序得到了。可以考虑结合Iterator模式来实现按照顺序的访问组件对象。
# 3.2 优缺点
1. 定义了包含基本对象和组合对象的类层次结构
在组合模式中,基本对象可以被组合成更复杂的组合对象,而组合对象又可以组合成更复杂的组合对象,可以不断地递归组合下去,从而构成一个统一的组合对象的类层次结构。
2. 统一了组合对象和叶子对象
在组合模式中,可以把叶子对象当作特殊的组合对象看待,为它们定义统一的父类,从而把组合对象和叶子对象的行为统一起来。
3. 简化了客户端调用
组合模式通过统一组合对象和叶子对象,使得客户端在使用它们的时候,就不需要再去区分它们,客户不关心使用的到底是什么类型的对象,这就大大简化了客户端的使用。
4. 更容易扩展
由于客户端是统一的面对Component来操作,因此,新定义的Composite或Leaf子类能够很容易的与已有的结构一起工作,而客户端不需要为增添了新的组件类而改变。
5. 很难限制组合中的组件类型
容易增加新的组件也会带来一些问题,比如很难限制组合中的组件类型。这在需要检测组件类型的时候,使得我们不能依靠编译期的类型约束来完成,必须在运行期间动态检测。
# 3.3 相关模式
1. 组合模式和装饰模式
这两个模式可以组合使用。
装饰模式在组装多个装饰器对象的时候,是一个装饰器找下一个装饰器,下一个再找下一个,如此递归下去。那么这种结构也可以使用组合模式来帮助构建,这样一来,装饰器对象就相当于组合模式的Composite对象了。
要让两个模式能很好的组合使用,通常会让它们有一个公共的父类,因此装饰器必须支持组合模式需要的一些功能,比如:增加、删除子组件等等。
2. 组合模式和享元模式
这两个模式可以组合使用。
如果组合模式中出现大量相似的组件对象的话,可以考虑使用享元模式来帮助缓存组件对象,这可以减少对内存的需要。
使用享元模式也是有条件的,如果组件对象的可变化部分的状态能够从组件对象里面分离出去,而且组件对象本身不需要向父组件发送请求的话,就可以采用享元模式。
3. 组合模式和迭代器模式
这两个模式可以组合使用。
在组合模式中,通常可以使用迭代器模式来遍历组合对象的子对象集合,而无需关心具体存放子对象的聚合结构。
4. 组合模式和访问者模式
这两个模式可以组合使用。
访问者模式能够在不修改原有对象结构的情况下,给对象结构中的对象增添新的功能。将访问者模式和组合模式合用,可以把原本分散在Composite和Leaf类中的操作和行为都局部化。
如果在使用组合模式的时候,预计到今后可能会有增添其它功能的可能,那么可以采用访问者模式,来预留好添加新功能的方式和通道,这样以后在添加新功能的时候,就不需要再修改已有的对象结构和已经实现的功能了。
5. 组合模式和职责链模式
这两个模式可以组合使用。
职责链模式要解决的问题是:实现请求的发送者和接收者之间解耦。职责链模式的实现方式是把多个接收者组合起来,构成职责链,然后让请求在这条链上传递,直到有接收者处理这个请求为止。
可以应用组合模式来构建这条链,相当于是子组件找父组件,父组件又找父组件,如此递归下去,构成一条处理请求的组件对象链。
6. 组合模式和命令模式
这两个模式可以组合使用。
命令模式中有一个宏命令的功能,通常这个宏命令就是使用组合模式来组装出来的。
# 四:JDK
- java.awt.Container#add(Component) (opens new window)
- java.util.Map#putAll(Map) (opens new window)
- java.util.List#addAll(Collection) (opens new window)
- java.util.Set#addAll(Collection) (opens new window)