👉文章示例代码👈

定义

将对象组合成树形结构以表示“部分-整体”的层次结构。

组合模式使得用户对单个对象和组合对象的使用具有一致性。

模式中的角色

  • 抽象构件Component:为组合中的叶节点对象和分支节点对象声明公共接口并实现它们的默认行为。
  • 树叶构件Leaf:是组合中的叶节点对象,叶节点没有子节点。
  • 树枝构件Composite:是组合中的分支节点对象,定义组件的行为,拥有子节点。

场景示例

笔者这里以旧版个人博客的导航菜单来举例。结构如下图所示。

image.png

首页、专题推荐、设计模式、Spring Cloud这些可以看作具体菜单项。编程手册则可以看作一个菜单目录,菜单目录下拥有设计模式和Spring Cloud这些菜单项。

菜单项是包含具体的页面访问地址的。

创建菜单组件类

创建一个菜单组件的抽象类,内部包含一些默认实现,交由子类决定是否重写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @author zhh
* @description 菜单组件类
* @date 2020-02-22 11:13
*/
public abstract class MenuComponent {

public void add(MenuComponent menuComponent) {
throw new UnsupportedOperationException("不支持添加操作");
}

public void remove(MenuComponent menuComponent) {
throw new UnsupportedOperationException("不支持删除操作");
}

public String getUrl(MenuComponent menuComponent) {
throw new UnsupportedOperationException("不支持获取地址操作");
}

public abstract String getName(MenuComponent menuComponent);

public abstract void print();
}

创建菜单项类

这里菜单项包含名称和页面访问地址两个属性。菜单项是不能进行菜单的 addremove 操作的,只有菜单目录是可以的。所以这里不重写上述的 addremove 方法。

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
/**
* @author zhh
* @description 菜单项类
* @date 2020-02-22 11:44
*/
public class MenuItem extends MenuComponent {

/**
* 菜单项名称
*/
private String name;

/**
* 页面访问地址
*/
private String url;

public MenuItem(String name, String url) {
this.name = name;
this.url = url;
}

@Override
public String getName(MenuComponent menuComponent) {
return this.name;
}

@Override
public String getUrl(MenuComponent menuComponent) {
return this.url;
}

@Override
public void print() {
System.out.println(String.format("%s, 菜单项的页面访问地址是: %s", name, url));
}
}

创建菜单目录类

菜单目录中可以包含很多的菜单项,而菜单项又是作为菜单组件。所以我们可以用一个容器属性去持有这些菜单组件。

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
/**
* @author zhh
* @description 菜单目录类
* @date 2020-02-22 15:03
*/
public class MenuCatalog extends MenuComponent {

/**
* 菜单目录名称
*/
private String name;

/**
* 菜单目录层级, 方便区分
*/
private Integer level;

/**
* 子菜单项列表
*/
private List<MenuComponent> menuItems = new ArrayList<MenuComponent>();

public MenuCatalog(String name, Integer level) {
this.name = name;
this.level = level;
}

@Override
public void add(MenuComponent menuComponent) {
menuItems.add(menuComponent);
}

@Override
public void remove(MenuComponent menuComponent) {
menuItems.remove(menuComponent);
}

@Override
public String getName(MenuComponent menuComponent) {
return this.name;
}

@Override
public void print() {
System.out.println(this.name);
for (MenuComponent menuComponent : menuItems) {
if (this.level != null) {
for (int i = 0; i < this.level; i++) {
System.out.print("*");
}
}
menuComponent.print();
}
}
}

测试类及输出

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
/**
* @author zhh
* @description 测试类
* @date 2020-02-22 15:15
*/
public class Test {

public static void main(String[] args) {
String site = "www.zhaohaihao.com";
// 首页
MenuComponent index = new MenuItem("首页", site);

// 编程手册
MenuComponent programmingManual = new MenuCatalog("编程手册", 2);
programmingManual.add(new MenuItem("设计模式", site + "/category/design-patterns"));
programmingManual.add(new MenuItem("Spring Cloud", site + "/category/spring-cloud"));

// 主题推荐
MenuComponent topic = new MenuItem("主题推荐", site + "/topic");

// 网站导航栏, 顶级目录, 目录层级用1标记
MenuComponent main = new MenuCatalog("网站导航目录", 1);
main.add(index);
main.add(programmingManual);
main.add(topic);

main.print();
}
}

测试类的输出结果如下:

网站导航目录
*首页, 菜单项的页面访问地址是: www.zhaohaihao.com
*编程手册
**设计模式, 菜单项的页面访问地址是: www.zhaohaihao.com/category/design-patterns
**Spring Cloud, 菜单项的页面访问地址是: www.zhaohaihao.com/category/spring-cloud
*主题推荐, 菜单项的页面访问地址是: www.zhaohaihao.com/topic

类结构图

以上示例类的结构图如下所示

image.png

由于菜单项类和菜单目录类都继承了菜单组件类,它们继承了菜单组件的所有行为。

透明方式与安全方式

组合模式有透明方式和安全方式两种。

透明方式

抽象构件声明了所有子类的全部方法,客户端无需区分叶节点对象和分支节点对象,对于客户端来说是透明的。

但是叶节点对象本身不包含子节点,而抽象构件又声明实现了一些针对于子节点的 addremove 等操作,这样会带来一些安全性的问题。

安全方式

安全方式与透明方式相反。

其将针对于子节点的 addremove 等操作移到了分支节点对象中,而抽象构件和叶节点对象并不包含这些方法,这样一来就避免了透明方式所带来的安全性问题。

但是由于分支节点对象和叶节点对象接口行为的不同,客户端需要区分叶节点对象和分支节点对象,所以就失去了透明性。

安全方式实现

上述场景示例中的代码实际上是透明方式的组合模式。笔者这里对上述场景示例代码进行调整,使用安全方式的组合模式来实现。

调整菜单组件类

移除菜单组件中的 addremovegetUrl 等操作。

1
2
3
4
5
6
7
8
public abstract class MenuComponent {

public abstract String getName(MenuComponent menuComponent);

public void print() {
throw new UnsupportedOperationException("不支持打印操作");
}
}

调整菜单项类

菜单项实现 getUrl 方法,而菜单目录则不实现该方法。

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
public class MenuItem extends MenuComponent {

/**
* 菜单项名称
*/
private String name;

/**
* 页面访问地址
*/
private String url;

public MenuItem(String name, String url) {
this.name = name;
this.url = url;
}

public String getUrl(MenuComponent menuComponent) {
return this.url;
}

@Override
public String getName(MenuComponent menuComponent) {
return this.name;
}

@Override
public void print() {
System.out.println(String.format("%s, 菜单项的页面访问地址是: %s", name, url));
}
}

调整菜单目录类

菜单目录实现 addremove 方法,而菜单项则不实现该方法。

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
public class MenuCatalog extends MenuComponent {

/**
* 菜单目录名称
*/
private String name;

/**
* 菜单目录层级, 方便区分
*/
private Integer level;

/**
* 子菜单项列表
*/
private List<MenuComponent> menuItems = new ArrayList<MenuComponent>();

public MenuCatalog(String name, Integer level) {
this.name = name;
this.level = level;
}

public void add(MenuComponent menuComponent) {
menuItems.add(menuComponent);
}

public void remove(MenuComponent menuComponent) {
menuItems.remove(menuComponent);
}

@Override
public String getName(MenuComponent menuComponent) {
return this.name;
}

@Override
public void print() {
System.out.println(this.name);
for (MenuComponent menuComponent : menuItems) {
if (this.level != null) {
for (int i = 0; i < this.level; i++) {
System.out.print("*");
}
}
menuComponent.print();
}
}
}

调整测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Test {

public static void main(String[] args) {
String site = "www.zhaohaihao.com";
// 首页
MenuComponent index = new MenuItem("首页", site);

// 编程手册
MenuCatalog programmingManual = new MenuCatalog("编程手册", 2);
programmingManual.add(new MenuItem("设计模式", site + "/category/design-patterns"));
programmingManual.add(new MenuItem("Spring Cloud", site + "/category/spring-cloud"));

// 主题推荐
MenuComponent topic = new MenuItem("主题推荐", site + "/topic");

// 网站导航栏, 顶级目录, 目录层级用1标记
MenuCatalog main = new MenuCatalog("网站导航目录", 1);
main.add(index);
main.add(programmingManual);
main.add(topic);

main.print();
}
}

类结构图

上述调整后类的结构图如下所示
image.png

总结

适用场景

  • 需求中体现部分与整体层次的结构。
  • 希望用户能够忽略组合对象与单个对象的差异,统一地使用组合结构中的所有对象。

优点

  • 能够清楚地定义分层次的复杂对象
  • 让用户忽略层次的差异,方便对整个层次结构进行控制
  • 简化客户端代码

缺点

  • 限制类型时会比较复杂
  • 使设计变得更加抽象

参考