组合模式用于将若干对象组合成树形结构,以表示"部分-整体"的层次结构。

  • 使客户端对单个对象和组合对象的使用具有一致性。
  • 只用于特定的树形组织场景,如文件管理、组织管理,能简化管理使代码变得简洁。

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

背景

你有两类对象:产品和盒子 。

  • 盒子中可以包含多个产品或者几个较小的盒子 。
  • 小盒子中同样可以包含一些产品或更小的盒子,以此类推。

假设在这些类的基础上开发一个定购系统。订单中可以包含无包装的简单产品,也可以包含装满产品的盒子以及其他盒子。此时如何计算每张订单的总价格呢?

可以尝试直接计算:打开所有盒子,找到每件产品,然后计算总价。

  • 这在真实世界中或许可行,但在程序中,不能简单地使用循环语句来完成该工作。
  • 因为必须事先知道所有产品和盒子的类别,所有盒子的嵌套层数以及其他繁杂的细节信息才能开始循环。
  • 因此,直接计算极不方便,甚至完全不可行。

解决方案

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

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

该方式的最大优点在于无需了解构成树状结构的对象的具体类,也无需了解对象是简单的产品还是复杂的盒子。

只需调用通用接口以相同的方式对其进行处理即可。调用该方法后,对象会将请求沿着树结构传递下去。

组合模式

使用场景

  • 如果需要实现树状对象结构,可以使用组合模式。
    • 组合模式提供了两种共享公共接口的基本元素类型:简单叶节点和复杂容器。容器中可以包含叶节点和其他容器,因此可以构建树状嵌套递归对象结构。
  • 如果希望客户端代码以相同方式处理简单和复杂元素,可以使用该模式。
    • 组合模式中定义的所有元素共用同一个接口。在这一接口的帮助下,客户端不必在意其所使用的对象的具体类。

实现步骤

  1. 确保应用的核心模型能够以树状结构表示。尝试将其分解为简单元素和容器。容器必须能够同时包含简单元素和其他容器。
  2. 声明组件接口及其一系列方法,这些方法对简单和复杂元素都有意义。
  3. 创建一个叶节点类表示简单元素。程序中可以有多个不同的叶节点类。
  4. 创建一个容器类表示复杂元素。在该类中,创建一个数组成员变量来存储对于其子元素的引用。该数组必须能够同时保存叶节点和容器,因此请确保将其声明为组合接口类型。
    • 实现组件接口方法时,记住容器应该将大部分工作交给其子元素来完成。
  5. 最后,在容器中定义添加和删除子元素的方法。

优缺点

优点

  • 可以利用多态和递归机制更方便地使用复杂树结构。
  • 开闭原则。无需更改现有代码,就可以在应用中添加新元素,使其成为对象树的一部分。

缺点

  • 对于功能差异较大的类,提供公共接口或许会有困难。在特定情况下,需要过度一般化组件的接口,但这会使其变得令人难以理解。

与其它模式的关系

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

示例

用一个操作系统文件系统的例子来理解组合模式。

文件系统中有两种类型的对象:文件和文件夹。在某些情形中,文件和文件夹应被视为相同的对象。 这就是组合模式发挥作用的时候了。

假设需要在文件系统中搜索特定的关键词,这一搜索操作需要同时作用于文件和文件夹上。

  • 对于文件而言,其只会查看文件的内容
  • 对于文件夹则会在其内部的所有文件中查找关键词。
1
2
3
4
5
// FileComponent 组件接口
type FileComponent interface {
	Search(string)
	GetName() string
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// File 叶子
type File struct {
	name string
}

func (f *File) Search(keyword string) {
	fmt.Printf("Searching for keyword %s in File %s\n", keyword, f.name)
}

func (f *File) GetName() string {
	return f.name
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// Folder 组合
type Folder struct {
	components []FileComponent
	name       string
}

func (f *Folder) Search(keyword string) {
	fmt.Printf("Serching recursively for keyword %s in Folder %s\n", keyword, f.name)
	for _, composite := range f.components {
		composite.Search(keyword)
	}
}

func (f *Folder) Add(c FileComponent) {
	// 这里添加的,可能是文件,也可能是文件夹
	f.components = append(f.components, c)
}

func (f *Folder) GetName() string {
	return f.name
}
 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
package main

func main() {
	/*
		-Folder2
		--Folder1
		---File1
		--File2
		--File3
	*/
	file1 := &File{name: "File1"}
	folder1 := &Folder{
		name: "Folder1",
	}
	folder1.Add(file1)

	folder2 := &Folder{
		name: "Folder2",
	}
	folder2.Add(folder1)

	file2 := &File{name: "File2"}
	file3 := &File{name: "File3"}
	folder2.Add(file2)
	folder2.Add(file3)

	folder2.Search("rose")
}

References

guru