初识设计模式——组合模式(Composite Pattern)

cuixiaogang

组合模式(Composite Pattern)是一种结构型设计模式,它允许你将对象组合成树形结构以表示 “部分 - 整体” 的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。

组合模式的构成

  • 抽象组件(Component):它是组合中所有对象的抽象接口,定义了叶子节点和组合节点的公共操作。
  • 叶子组件(Leaf):它是组合中的最小单位,没有子节点,实现了抽象组件的所有操作。
  • 组合组件(Composite):它包含了子组件(可以是叶子组件或其他组合组件),实现了抽象组件的所有操作,同时还提供了管理子组件的方法,如添加、删除子组件等。

组合模式结构图

案例

场景

在 WEB 开发中,菜单系统是一个典型的使用组合模式的案例。菜单可以包含子菜单,子菜单又可以包含子菜单或菜单项,使用组合模式可以方便地管理菜单的层次结构。

代码

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
<?php
// 抽象组件
abstract class MenuComponent {
public function add(MenuComponent $menuComponent) {
throw new Exception("不支持的操作");
}

public function remove(MenuComponent $menuComponent) {
throw new Exception("不支持的操作");
}

public function getChild($index) {
throw new Exception("不支持的操作");
}

public function getName() {
throw new Exception("不支持的操作");
}

public function getUrl() {
throw new Exception("不支持的操作");
}

public function display() {
throw new Exception("不支持的操作");
}
}

// 叶子组件:菜单项
class MenuItem extends MenuComponent {
private $name;
private $url;

public function __construct($name, $url) {
$this->name = $name;
$this->url = $url;
}

public function getName() {
return $this->name;
}

public function getUrl() {
return $this->url;
}

public function display() {
echo "<a href='{$this->getUrl()}'>{$this->getName()}</a><br>";
}
}

// 组合组件:菜单
class Menu extends MenuComponent {
private $name;
private $menuComponents = [];

public function __construct($name) {
$this->name = $name;
}

public function add(MenuComponent $menuComponent) {
$this->menuComponents[] = $menuComponent;
}

public function remove(MenuComponent $menuComponent) {
$key = array_search($menuComponent, $this->menuComponents);
if ($key!== false) {
unset($this->menuComponents[$key]);
}
}

public function getChild($index) {
return $this->menuComponents[$index];
}

public function getName() {
return $this->name;
}

public function display() {
echo "<strong>{$this->getName()}</strong><br>";
foreach ($this->menuComponents as $menuComponent) {
$menuComponent->display();
}
}
}

// 客户端代码
$mainMenu = new Menu("主菜单");

$subMenu1 = new Menu("子菜单1");
$subMenu1->add(new MenuItem("菜单项1.1", "page1-1.php"));
$subMenu1->add(new MenuItem("菜单项1.2", "page1-2.php"));

$subMenu2 = new Menu("子菜单2");
$subMenu2->add(new MenuItem("菜单项2.1", "page2-1.php"));
$subMenu2->add(new MenuItem("菜单项2.2", "page2-2.php"));

$mainMenu->add($subMenu1);
$mainMenu->add($subMenu2);

$mainMenu->display();
?>

代码解释

  • MenuComponent 是抽象组件,定义了菜单组件的公共操作。
  • MenuItem 是叶子组件,表示菜单项。
  • Menu 是组合组件,表示菜单,它可以包含菜单项或子菜单。
  • 客户端代码创建了一个主菜单和两个子菜单,并将子菜单添加到主菜单中,最后显示主菜单。

UML类图

UML类图

组合模式的适用场景

  • 表示对象的部分 - 整体层次结构:当你需要表示一个对象的层次结构,并且希望用户可以忽略单个对象和组合对象的区别时,可以使用组合模式。
  • 统一处理单个对象和组合对象:当你需要对单个对象和组合对象进行统一的操作时,组合模式可以让你编写简单的代码来处理它们。

更具体的业务场景

1. 菜单与导航栏系统

  • 描述:在网站或应用程序里,菜单和导航栏往往具备层次结构。主菜单可能包含多个子菜单,子菜单又能包含菜单项或者更深层次的子菜单。组合模式能够让开发者以统一的方式处理菜单、子菜单以及菜单项。
  • 示例:电商网站的导航栏,“商品分类” 为主菜单,下面有 “服装”“电子产品” 等子菜单,“服装” 子菜单下又有 “上衣”“裤子” 等菜单项。借助组合模式,能方便地添加、删除和显示菜单元素。

2. 组织结构管理

  • 描述:企业、学校等组织都有自己的层级结构。组合模式可以用于表示这种组织结构,每个部门可以是一个组合对象,员工则是叶子对象。这样可以统一处理部门和员工的信息。
  • 示例:一家公司有多个部门,如研发部、市场部等,研发部下面又有不同的项目组,每个项目组有多个员工。通过组合模式,能够方便地统计各部门、项目组的人员数量,或者对整个组织进行统一的操作。

3. 文件系统管理

  • 描述:文件系统是典型的树形结构,包含目录(文件夹)和文件。目录可以包含子目录和文件,使用组合模式可以统一处理目录和文件的操作,如列出内容、计算大小等。
  • 示例:在操作系统中,用户可以对一个文件夹执行查看其包含的所有文件和子文件夹的操作,也可以对单个文件进行查看属性等操作。组合模式使得对文件夹和文件的操作逻辑可以统一实现。

4. 图形界面组件布局

  • 描述:在图形用户界面(GUI)开发中,界面组件通常以树形结构组织。一个窗口可以包含多个面板,每个面板又可以包含按钮、文本框等子组件。组合模式可以统一管理这些组件的添加、删除和绘制操作。
  • 示例:在一个游戏的设置界面中,主窗口包含多个设置面板,每个面板上有不同的按钮和滑块用于调整游戏参数。通过组合模式,可以方便地对整个界面进行布局和管理。

5. 报表与数据统计

  • 描述:在生成复杂报表时,报表可能包含多个子报表和数据项。组合模式可以将子报表和数据项统一管理,方便进行数据的汇总和展示。
  • 示例:财务报表可能包含资产负债表、利润表等子报表,每个子报表又包含多个数据项。使用组合模式可以方便地计算和展示整个报表的数据。

组合模式的优缺点

优点

  • 简化客户端代码:客户端可以统一处理单个对象和组合对象,无需关心对象是单个还是组合。
  • 易于扩展:可以很方便地添加新的叶子组件或组合组件,符合开闭原则。

缺点

  • 设计复杂:组合模式可能会导致设计变得复杂,特别是当层次结构较深时。
  • 限制类型安全:由于组合模式允许在组合中添加不同类型的对象,可能会导致类型安全问题。