架构师内功心法,作为树形结构系统架构的组合模式详解


title: 架构师内功心法,作为树形结构系统架构的组合模式详解
date: 2020-03-10 14:08:50
tags:

在古代皇帝要想管理整个国家,他是不可能直接管理到具体每一个老百姓的,因此皇帝设置了很多机构,比如三省六部,这些机构下面又有很多小的组织,它们共同管理着整个国家。再比如有一个很大的公司,下面有很多部门,每一个部门下面又有很多个部门,这些就是组合模式的体现。比如树形菜单,操作系统目录结构等,所以组合模式在我们生活中也是非常常见的。


一、组合模式的应用场景

组合模式(Composite Pattern)也称为整体-部分(Part-Whole)模式,它的宗旨是通过将单个对象(叶子节点)和组合对象(树枝节点)用相同的接口进行表示,使得客户对单个对象和组合对象使用具有一致性。

1.1 组合关系与聚和关系的区别

组合关系:在古代皇宫的三宫六院,贵妃很多,但是每一个贵妃都属于一个皇帝(具有相同的生命周期);

聚和关系:一个老师有很多学生,但是每一个学生也属于多个老师(具有不同的生命周期)。

组合模式一般用来描述整体与部分的关系,它将对象组织到树形结构中,最顶层的节点成为根节点,根节点下面包括树枝节点和叶子节点,树枝节点下面又包括树枝节点和叶子节点。


由上图可以看出,其实根节点和树枝节点本质上是同一种数据类型,可以作为容器使用;而叶子节点与树枝节点在语义上不属于同一种类型,但是在组合模式中,会把树枝节点和叶子节点认为是同一种数据类型(用同一接口定义),让它们具备一致行为。这样,在组合模式中,整个树形结构中的对象都是同一种类型,带来的一个好处就是客户无需辨别树枝节点还是叶子节点,而是可以直接进行操作,给客户使用带来极大的便利。

1.2 组合模式的角色

组合模式包含 3 个角色:

  • 抽象根节点(Component):定义系统各层次对象的共有方法和属性,可以预先定义一些默认行为和属性;
  • 树枝节点(Composite):定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个树形结构;
  • 叶子节点(Leaf):叶子节点对象,其下再无分支,是系统层次遍历的最小单位。

组合模式在代码具体实现上,有两种不同的方式,分别是透明组合模式和安全组合模式。

1.3 组合模式应用场景

当子系统与其内各个对象层次呈现树形结构时,可以使用组合模式让子系统内各个对象层次的行为操作具备一致性。客户端使用该子系统内任意一个层次对象时,无须进行区分,直接使用通用操作即可,为客户端的使用带来了便捷。

组合模式有以下两个应用场景:

  • 希望客户端可以忽略组合对象与单个对象的差异时;
  • 对象层次具备整体和部分,呈树形结构。

1.4 透明组合模式的写法

透明组合模式是把所有的公共方法都定义在Component中,这样做的好处是客户度无需分辨树枝节点(Composite)和叶子节点(Leaf),它们具备完全一致的接口。来看一下UML类图吧:

下面来看一个实际的案例吧。以某IT教育培训的课程为例,来设计一个课程的关系结构。有嵌入式编程、Web前端、Java编程、云计算、Hadoop、Flink等课程。而云计算、Hadoop、Flink都属于大数据系列课程,每个课程的定价也是不一样的。但是这些课程不管怎么组合,都有一些共性,而且都是整体和部分的关系,可以用组合模式来设计。先创建一个顶层的抽象类 CourseComponent:

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

public void addChild(CourseComponent catalogComponent) {
throw new UnsupportedOperationException("不支持添加操作!");
}

public void removeChild(CourseComponent catalogComponent) {
throw new UnsupportedOperationException("不支持删除操作!");
}

public String getName(CourseComponent catalogComponent) {
throw new UnsupportedOperationException("不支持获取名称操作!");
}

public double getPrice(CourseComponent catalogComponent) {
throw new UnsupportedOperationException("不支持获取价格操作!");
}

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

把所有可能用到的方法都定义到这个最顶层的抽象类中,但是不写任何逻辑处理的代码,而是直接抛异常。这里,有些小伙伴会有疑惑,为什么不用抽象方法?因为,用了抽象方法,其子类就必须实现,这样便体现不出各子类的细微差异。 因此,子类继承此抽象类后,只需要重写有差异的方法覆盖父类的方法即可。接下来分别创建课程类Course 和课程包 CoursePackage类。先创建Course类:

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
public class Course extends CourseComponent {

private String name;

private double price;

public Course(String name, double price) {
this.name = name;
this.price = price;
}

@Override
public String getName(CourseComponent catalogComponent) {
return this.name;
}

@Override
public double getPrice(CourseComponent catalogComponent) {
return this.price;
}

@Override
public void print() {
System.out.println(name + " (¥" + price + "元)");
}

}

再创建CoursePackage类:

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
public class CoursePackage extends CourseComponent {

private List<CourseComponent> items = new ArrayList<>();
private String name;
private Integer level;

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

@Override
public void addChild(CourseComponent catalogComponent) {
items.add(catalogComponent);
}

@Override
public void removeChild(CourseComponent catalogComponent) {
items.remove(catalogComponent);
}

@Override
public String getName(CourseComponent catalogComponent) {
return this.name;
}

@Override
public void print() {
System.out.println(name);
for(CourseComponent courseComponent : items) {
//控制显示格式
if(this.level != null){
for(int i = 0; i < this.level; i ++){
//打印空格控制格式
System.out.print(" ");
}
for(int i = 0; i < this.level; i ++){
//每一行开始打印一个+号
if(i == 0){
System.out.print("+");
}
System.out.print("-");
}
//打印标题
courseComponent.print();
}
}
}
}

测试main方法:

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
public static void main(String[] args) {
System.out.println("=============透明组合模式=============");

CourseComponent course = new Course("嵌入式编程", 3000.00);
CourseComponent webFront = new Course("Web前端", 10000.00);
CourseComponent java = new Course("Java编程", 8000.00);

CoursePackage bigData = new CoursePackage("大数据", 2);

CourseComponent cloudCalc = new Course("云计算", 3000.00);
CourseComponent hadoop = new Course("hadoop", 3500.00);
CourseComponent flink = new Course("flink", 4000.00);
bigData.addChild(cloudCalc);
bigData.addChild(hadoop);
bigData.addChild(flink);

CourseComponent catalog = new CoursePackage("课程主目录",1);
catalog.addChild(course);
catalog.addChild(webFront);
catalog.addChild(java);
catalog.addChild(bigData);

catalog.print();

}

运行结果:

透明组合模式把所有公共方法都定义在Component中,这样做的好处是客户端无需分辨是叶子节点(Leaf)和树枝节点(Composite),它们具备完全一致的接口;缺点是叶子节点(Leaf)会继承得到一些它所不需要(管理子类操作的方法)的方法,这与设计模式 接口隔离原则相违背。

为了让大家更加透彻理解,下面我们来看安全组合模式的写法。

1.5 安全组合模式的写法

安全组合模式是只规定系统各个层次的最基础的一致行为,而把组合(树节点)本身的方法(管理子类对象的添加,删除等)放到自身当中。其 UML 类图如下所示:

再举一个大家都熟悉的例子吧。电脑里面的文件系统其实就是一个典型的树形结构,目录包含文件夹和文件,文件夹里面又包含文件夹和文件……接下来就用代码来实现这个目录系统。
文件系统有两个大的层次:文件夹,文件。其中,文件夹能容纳其他层次,为树枝节点;文件为最小单位,为叶子节点。由于目录系统层次较少,且树枝节点(文件夹)结构相对稳定,而文件其实可以有很多类型,所以这里我们选择使用 安全组合模式 来实现目录系统,可以避免为叶子类型(文件)引入冗余方法。先创建最顶层的抽象组件 Directory 类:

1
2
3
4
5
6
7
8
9
10
public abstract class Directory {

protected String name;

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

public abstract void show();
}

然后分别创建Folder和File类:

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
public class Folder extends Directory {

private List<Directory> dirs;

private Integer level;

public Folder(String name, Integer level) {
super(name);
this.level = level;
this.dirs = new ArrayList<>();
}

public boolean add(Directory dir) {
return dirs.add(dir);
}

public boolean remove(Directory dir) {
return dirs.remove(dir);
}

public Object get(int index) {
return dirs.get(index);
}

public void list() {
for(Directory dir : dirs) {
System.out.println(dir.name);
}
}

@Override
public void show() {
System.out.println(this.name);
for (Directory dir : this.dirs) {
//控制显示格式
if (this.level != null) {
for (int i = 0; i < this.level; i++) {
//打印空格控制格式
System.out.print(" ");
}
for (int i = 0; i < this.level; i++) {
//每一行开始打印一个+号
if (i == 0) {
System.out.print("+");
}
System.out.print("-");
}
}
//打印名称
dir.show();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
public class File extends Directory {

public File(String name) {
super(name);
}

@Override
public void show() {
System.out.println(this.name);
}
}

测试main方法:

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
public static void main(String[] args) {
System.out.println("==========安全组合模式=============");
File chrome = new File("谷歌浏览器");
File safe360 = new File("360浏览器");
File edge = new File("微软Edge浏览器");

Folder browser = new Folder("浏览器", 2);
browser.add(chrome);
browser.add(safe360);
browser.add(edge);

File wx = new File("wechat.exe");
File qq = new File("QQ.exe");
File dd = new File("DingDing.exe");

Folder root = new Folder("root", 1);
root.add(wx);
root.add(qq);
root.add(dd);
root.add(browser);

System.out.println("----------show()方法效果-----------");
root.show();
System.out.println("----------list()方法效果-----------");
root.list();
}

运行结果:


安全组合模式的好处是接口定义职责清晰,符合设计模式单一职责原则和接口隔离原则;缺点是客户需要区分树枝节点(Composite)和叶子节点(Leaf),这样才能正确处理各个层次的操作,客户端无法依赖抽象(Component),违背了设计模式依赖倒置原则。

二、组合模式在源码中的体现

2.1 HashMap中的putAll()方法

组合模式在源码中应用也是非常广泛的。首先我们来看一个非常熟悉的HashMap,他里面有一个putAll()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void putAll(Map<? extends K, ? extends V> m) {
putMapEntries(m, true);
}

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
else if (s > threshold)
resize();
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}

putAll()方法传入的是Map对象,Map就是一个抽象构件(同时这个构件中只支持键值对的存储方式),而HashMap是一个中间构件,HashMap中间的Node节点是叶子节点。说到中间构件就会有规定的存储方式。HashMap中的存储方式是一个静态内部类的数组Node<K,V>,源码如下:

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
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;

Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}

public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }

public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}

public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}

public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}

2.2 ArrayList中的addAll()方法

常用的 ArrayList 对象也有 addAll()方法,其参数也是 ArrayList 的父类 Collection,来看源代码:

1
2
3
4
5
6
7
8
 public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}

组合对象和被组合对象都应该有统一的接口实现或者统一的抽象父类。

三、组合模式的优缺点

优点:

  • 清楚地定义分层次的复杂对象,表示对象的全部或部分层次;
  • 让客户端忽略了层次的差异,方便对整个层次结构进行控制;
  • 简化客户端代码;
  • 符合开闭原则。

缺点:

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

-