本博客大部分内容来于免费在线学习设计模式

1:组合模式

组合模式是一种结构型设计模式,又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象,你可以使用它将对象组合成树状结构,并且能像使用独立对象一样使用它们。

2:组合模式问题

如果应用的核心模型能用树状结构表示,在应用中使用组合模式才有价值。

例如,你有两类对象:产品盒子。一个盒子中可以包含多个产品或者几个较小的盒子。这些小盒子中同样可以包含一些产品或更小的盒子,以此类推。

在这些类的基础上你需要开发一个订单系统。订单中可以包含无包装的简单产品,也可以包含装满产品的盒子……以及其他盒子。如何计算订单的总价呢?

20200831120109

在现实生活中,可以直接拆开盒子,直接计算每个产品的价格。而在程序中,你必须知道所有产品和盒子的细节,知道盒子的嵌套层数以及其他详细信息,才能计算出总价。因此直接计算是不可行。

在现实生活中,存在很多“部分-整体”的关系,例如,大学中的部门与学院、总公司中的部门与分公司、学习用品中的书与书包、生活用品中的衣服与衣柜以及厨房中的锅碗瓢盆等。在软件开发中也是这样,例如,文件系统中的文件与文件夹、窗体程序中的简单控件与容器控件等。对这些简单对象与复合对象的处理,如果用组合模式来实现会很方便。

3:组合模式解决方案

组合模式建议使用一个通用接口来与产品和盒子进行交互, 并且在该接口中声明一个计算总价的方法。

例如:对于一个产品,该方法直接返回其价格;对于一个盒子,该方法遍历盒子中的所有项目,询问每个项目的价格,然后返回该盒子的总价格。如果其中某个项目是小一号的盒子,那么当前盒子也会遍历其中的所有项目,以此类推,直到计算出所有内部组成部分的价格。还可以在盒子的最终价格中增加额外费用,作为该盒子的包装费用。

该方式的最大优点在于客户端代码可以一致地处理单个对象和组合对象,无须关心自己处理的是单个对象,还是组合对象。只需调用通用接口以相同的方式对其进行处理即可。当你调用该方法后,对象会将请求沿着树结构传递下去。

4:组合模式结构

20200831120959

  1. 组件 (Component) 接口描述了树中简单项目和复杂项目所共有的操作。
  2. 叶节点 (Leaf) 是树的基本结构, 它不包含子项目。
    一般情况下, 叶节点最终会完成大部分的实际工作, 因为它们无法将工作指派给其他部分。
  3. 容器 (Container)——又名 “组合 (Composite)”——是包含叶节点或其他容器等子项目的单位。 容器不知道其子项目所属的具体类, 它只通过通用的组件接口与其子项目交互。
    容器接收到请求后会将工作分配给自己的子项目, 处理中间结果, 然后将最终结果返回给客户端。
  4. 客户端 (Client) 通过组件接口与所有项目交互。 因此, 客户端能以相同方式与树状结构中的简单或复杂项目交互。

代码示例:

1
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// 客户端:通过组件和叶节点,容器交互
public class CompositeTest {
public static void main(String[] args) {
double sum = 0;
Box bigBox, mediumBox, smallRedBox, smallWhiteBox;
Good good;
bigBox = new Box("大盒子");
mediumBox = new Box("中等盒子");
smallRedBox = new Box("小红盒子");
smallWhiteBox = new Box("小白盒子");

good = new Good("耳机", 1, 200);
smallWhiteBox.add(good);

good = new Good("充电线", 2, 100);
smallRedBox.add(good);

good = new Good("手机", 1, 4000);
mediumBox.add(good);
mediumBox.add(smallRedBox);
mediumBox.add(smallWhiteBox);

good = new Good("钢化膜", 5, 20);
bigBox.add(good);
bigBox.add(mediumBox);

bigBox.show();
}
}

// 组件:计算总价
// 组件接口会声明组合中简单和复杂对象的通用操作。
interface Article {
double calculate();

void show();
}

// 叶节点:商品
// 叶节点类代表组合的终端对象。叶节点对象中不能包含任何子对象。
// 叶节点对象通常会完成实际的工作,组合对象则仅会将工作委派给自己的子部件。
class Good implements Article {
// 商品名称
private String name;
// 商品数量
private int number;
// 商品单价
private double price;

// 构造函数
public Good(String name, int number, double price) {
this.name = name;
this.number = number;
this.price = price;
}

// 计算商品总价
@Override
public double calculate() {
return number * price;
}

// 展示商品细节
@Override
public void show() {
System.out.println("name: " + name + " number: " + number + " price: " + price + " 总价: " + calculate());
}
}

// 容器:盒子
// 组合类表示可能包含子项目的复杂组件。组合对象通常会将实际工作委派给子项目,然后“汇总”结果。
class Box implements Article {
// 盒子名称
private String name;
// 包含的叶节点或容器
private ArrayList<Article> articles = new ArrayList<>();

public Box(String name) {
this.name = name;
}

// 添加商品或盒子
public void add(Article article) {
articles.add(article);
}

// 移除
public void remove(Article article) {
articles.remove(article);
}

// 计算盒子总价
@Override
public double calculate() {
double sum = 0;
// 访问每个组件,如果是商品直接计算,是盒子遍历所有项目
for (Object obj : articles) {
sum += ((Article) obj).calculate();
}
return sum;
}

// 盒子展示
@Override
public void show() {
System.out.println("盒子: " + name + " 总价:" + calculate());
// 访问每个组件,如果是商品直接展示,是盒子遍历所有项目
for (Object obj : articles) {
((Article) obj).show();
}
}
}

// 输出如下:
// 盒子: 大盒子 总价:4500.0
// name: 钢化膜 number: 5 price: 20.0 总价: 100.0
// 盒子: 中等盒子 总价:4400.0
// name: 手机 number: 1 price: 4000.0 总价: 4000.0
// 盒子: 小红盒子 总价:200.0
// name: 充电线 number: 2 price: 100.0 总价: 200.0
// 盒子: 小白盒子 总价:200.0
// name: 耳机 number: 1 price: 200.0 总价: 200.0

5:组合模式适用场景

  1. 如果你需要实现树状对象结构, 可以使用组合模式。
  2. 如果你希望客户端代码以相同方式处理简单和复杂元素, 可以使用该模式。

6:组合模式优缺点

优点 缺点
你可以利用多态和递归机制更方便地使用复杂树结构。 对于功能差异较大的类, 提供公共接口或许会有困难。 在特定情况下, 你需要过度一般化组件接口, 使其变得令人难以理解。
开闭原则。 无需更改现有代码, 你就可以在应用中添加新元素, 使其成为对象树的一部分。

7:组合模式与其他模式关系

  • 桥接模式、状态模式和策略模式(在某种程度上包括适配器模式)模式的接口非常相似。实际上,它们都基于组合模式——即将工作委派给其他对象,不过也各自解决了不同的问题。模式并不只是以特定方式组织代码的配方,你还可以使用它们来和其他开发者讨论模式所解决的问题。
  • 你可以在创建复杂组合树时使用生成器模式,因为这可使其构造步骤以递归的方式运行。
  • 责任链模式通常和组合模式结合使用。在这种情况下,叶组件接收到请求后,可以将请求沿包含全体父组件的链一直传递至对象树的底部。
  • 你可以使用迭代器模式来遍历组合树。
  • 你可以使用访问者模式对整个组合树执行操作。
  • 你可以使用享元模式实现组合树的共享叶节点以节省内存。
  • 组合和装饰模式的结构图很相似, 因为两者都依赖递归组合来组织无限数量的对象。
  • 装饰类似于组合, 但其只有一个子组件。 此外还有一个明显不同: 装饰为被封装对象添加了额外的职责, 组合仅对其子节点的结果进行了 “求和”。
  • 但是, 模式也可以相互合作: 你可以使用装饰来扩展组合树中特定对象的行为。
  • 大量使用组合和装饰的设计通常可从对于原型模式的使用中获益。 你可以通过该模式来复制复杂结构, 而非从零开始重新构造。

8:组合模式举例

组合模式计算盒子的总价如下

组合模式

  • 首先 a = b + h + j
    • 因为b是盒子,所以 b = c + f
      • c也是盒子,c = d + e;d,e是商品,不需要再往下计算
      • f是盒子,但是只含有一个商品,所以 f = g
    • h和f相同,只含有一个商品,所以 h = i
    • i是商品,不需要计算

9:参考