设计模式之组合模式

组合模式

组合模式

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

组合关系与聚合关系的区别:

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

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

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

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

组合模式包含3个角色:

1、抽象根节点(Component):定义系统各层次对象的共有方法和属性,可以预先定义一些默认行为和属性;

2、树枝节点(Composite):定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个树形结构;

3、叶子节点(Leaf):叶子节点对象,其下再无分支,是系统层次遍历的最小单位。

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

组合模式的应用场景

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

如果树形结构系统不使用组合模式进行架构,那么按照正常的思维逻辑,对该系统进行职责分析,按上文树形结构图所示,该系统具备两种对象层次类型:树枝节点和叶子节点。那么我们就需要构造两种对应的类型,然后由于树枝节点具备容器功能,因此树枝节点类内部需维护多个集合存储其他对象层次(如:List<Composite>List<Leaf>)如果当前系统对象层次更复杂时,那么树枝节点内就又要增加对应的层次集合,这对树枝节点的构建带来了巨大的复杂性,臃肿性以及不可扩展性。同时客户端访问该系统层次时,还需进行层次区分,这样才能使用对应的行为,给客户端的使用也带来了巨大的复杂性。而如果使用组合模式构建该系统,由于组合模式抽取了系统各个层次的共性行为,具体层次只需按需实现所需行为即可,这样子系统各个层次就都属于同一种类型,所以树枝节点只需维护一个集合(List<Component>)即可存储系统所有层次内容,并且客户端也无需区分该系统各个层次对象,对内系统架构简洁优雅,对外接口精简易用。

组合模式主要总结为以下应用场景:

1、希望客户端可以忽略组合对象与单个对象的差异时;

2、对象层次具备整体和部分,呈树形结构。

在我们生活中的组合模式也非常常见,比如树形菜单,操作系统目录结构,公司组织架构等。

透明组合模式的写法

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

Component 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public abstract class Component
{
protected void operation()
{
throw new UnsupportedOperationException("不支持的操作");
}
protected boolean addChild(Component component)
{
throw new UnsupportedOperationException("不支持添加操作");
}
protected boolean removeChild(Component component)
{
throw new UnsupportedOperationException("不支持删除操作");
}
protected Component getChild(int i)
{
throw new UnsupportedOperationException("不支持获取操作");
}
}

Composite 树枝节点

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
public class Composite extends Component
{
private String name;
private Integer level;

private List<Component> componentList = new ArrayList<>();

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

@Override
public void operation()
{
System.out.println(name + " 树枝子节点操作 下属节点个数:" + componentList.size());

for (Component component : componentList)
{
//控制显示格式
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("-");
} //打印标题
component.operation();
}
}
}

@Override
public boolean addChild(Component component)
{
return componentList.add(component);
}

@Override
public boolean removeChild(Component component)
{
return componentList.remove(component);
}

@Override
public Component getChild(int i)
{
return componentList.get(i);
}
}

Leaf 叶子节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Leaf extends Component
{
private String name;

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

@Override
public void operation()
{
System.out.println(name + " 叶子节点操作");
}
}

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Main
{
public static void main(String[] args)
{
Component composite = new Composite("树枝节点", 2);

Component leaf1 = new Leaf("叶子节点1");
Component leaf2 = new Leaf("叶子节点2");
composite.addChild(leaf1);
composite.addChild(leaf2);

Component topComposite = new Composite("顶层节点", 1);
Component leaf3 = new Leaf("叶子节点3");

topComposite.addChild(composite);
topComposite.addChild(leaf3);

topComposite.operation();
}
}

输出结果

1
2
3
4
5
顶层节点 树枝子节点操作 下属节点个数:2
+-树枝节点 树枝子节点操作 下属节点个数:2
+--叶子节点1 叶子节点操作
+--叶子节点2 叶子节点操作
+-叶子节点3 叶子节点操作

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

安全组合模式的写法

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

Component 接口

1
2
3
4
5
6
7
public abstract class Component
{
protected void operation()
{
throw new UnsupportedOperationException("不支持的操作");
}
}

Composite 树枝节点

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
public class Composite extends Component
{
private String name;
private Integer level;

private List<Component> componentList = new ArrayList<>();

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

@Override
public void operation()
{
System.out.println(name + " 树枝子节点操作 下属节点个数:" + componentList.size());

for (Component component : componentList)
{
//控制显示格式
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("-");
} //打印标题
component.operation();
}
}
}

public boolean addChild(Component component)
{
return componentList.add(component);
}

public boolean removeChild(Component component)
{
return componentList.remove(component);
}

public Component getChild(int i)
{
return componentList.get(i);
}
}

Leaf 叶子节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Leaf extends Component
{
private String name;

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

@Override
public void operation()
{
System.out.println(name + " 叶子节点操作");
}
}

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Main
{
public static void main(String[] args)
{
Composite composite = new Composite("树枝节点", 2);

Leaf leaf1 = new Leaf("叶子节点1");
Leaf leaf2 = new Leaf("叶子节点2");
composite.addChild(leaf1);
composite.addChild(leaf2);

Composite topComposite = new Composite("顶层节点", 1);
Leaf leaf3 = new Leaf("叶子节点3");

topComposite.addChild(composite);
topComposite.addChild(leaf3);

topComposite.operation();
}
}

输出结果

1
2
3
4
5
顶层节点 树枝子节点操作 下属节点个数:2
+-树枝节点 树枝子节点操作 下属节点个数:2
+--叶子节点1 叶子节点操作
+--叶子节点2 叶子节点操作
+-叶子节点3 叶子节点操作

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

组合模式在源码中的应用

组合模式在源码中应用也是非常广泛的,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
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
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
...
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);
}
}
}
...
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
...
}

putAll() 方法传入的是Map对象,Map就是一个抽象构件(同时这个构件中只支持键值对的存储格式),而HashMap是一个中间构件,HashMap中的Node节点就是叶子节点,中间构件一般就会有规定的存储方式。

HashMap中的存储方式是一个静态内部类的数组Node<K,V>[] tab,其源码如下:

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;
}
}

组合模式的优缺点

既然组合模式会被分为两种实现,那么肯定是不同的场合某一种会更加适合,也即具体情况具体分析。

透明组合模式将公共接口封装到抽象根节点(Component)中,那么系统所有节点就具备一致行为,所以如果当系统绝大多数层次具备相同的公共行为时,采用透明组合模式也许会更好(代价:为剩下少数层次节点引入不需要的方法);而如果当系统各个层次差异性行为
较多或者树节点层次相对稳定(健壮)时,采用安全组合模式

设计模式的出现并不是说我们要写的代码一定要遵循设计模式所要求的方方面面,这是不现实同时也是不可能的。设计模式的出现,其实只是强调好的代码所具备的一些特征(六大设计原则),这些特征对于项目开发是具备积极效应的,但不是说我们每实现一个类就一定要全部满足设计模式的要求,如果真的存在完全满足设计模式的要求,反而可能存在过度设计的嫌疑。同时,23种设计模式,其实都是严格依循设计模式六大原则进行设计,只是不同的模式在不同的场景中会更加适用。设计模式的理解应该重于意而不是形,真正编码时,经常使用的是某种设计模式的变形体真正切合项目的模式才是正确的模式。

组合模式的优缺点

优点:

1、清楚地定义分层次的复杂对象,表示对象的全部或部分层次

2、让客户端忽略了层次的差异,方便对整个层次结构进行控制

3、简化客户端代码

4、符合开闭原则

缺点:

1、限制类型时会较为复杂

2、使设计变得更加抽象

打赏

请我喝杯咖啡吧~

支付宝
微信