组合模式

组合模式

将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。

组合模式(Composite)经常用于树形结构,为了简化代码,使用Composite可以把一个叶子节点与一个父节点统一起来处理。

我们来看一个具体的例子。在XML或HTML中,从根节点开始,每个节点都可能包含任意个其他节点,这些层层嵌套的节点就构成了一颗树。

要以树的结构表示XML,我们可以先抽象出节点类型Node

1
2
3
4
5
6
7
8
public interface Node {
// 添加一个节点为子节点:
Node add(Node node);
// 获取子节点:
List<Node> children();
// 输出为XML:
String toXml();
}

对于一个这样的节点,我们称之为 ElementNode,它可以作为容器包含多个子节点:

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 ElementNode implements Node {
private String name;
private List<Node> list = new ArrayList<>();

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

public Node add(Node node) {
list.add(node);
return this;
}

public List<Node> children() {
return list;
}

public String toXml() {
String start = "<" + name + ">\n";
String end = "</" + name + ">\n";
StringJoiner sj = new StringJoiner("", start, end);
list.forEach(node -> {
sj.add(node.toXml() + "\n");
});
return sj.toString();
}
}

对于普通文本,我们把它看作TextNode,它没有子节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TextNode implements Node {
private String text;

public TextNode(String text) {
this.text = text;
}

public Node add(Node node) {
throw new UnsupportedOperationException();
}

public List<Node> children() {
return List.of();
}

public String toXml() {
return text;
}
}

此外,还可以有注释节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class CommentNode implements Node {
private String text;

public CommentNode(String text) {
this.text = text;
}

public Node add(Node node) {
throw new UnsupportedOperationException();
}

public List<Node> children() {
return List.of();
}

public String toXml() {
return "<!-- " + text + " -->";
}
}

通过ElementNodeTextNodeCommentNode,我们就可以构造出一颗树:

1
2
3
4
5
6
7
8
9
Node root = new ElementNode("school");
root.add(new ElementNode("classA")
.add(new TextNode("Tom"))
.add(new TextNode("Alice")));
root.add(new ElementNode("classB")
.add(new TextNode("Bob"))
.add(new TextNode("Grace"))
.add(new CommentNode("comment...")));
System.out.println(root.toXml());

最后通过root节点输出的XML如下:

1
2
3
4
5
6
7
8
9
10
11
<school>
<classA>
Tom
Alice
</classA>
<classB>
Bob
Grace
<!-- comment... -->
</classB>
</school>

可见,使用Composite模式时,需要先统一单个节点以及“容器”节点的接口:

1
2
3
4
5
6
7
8
9
             ┌───────────┐
│ Node │
└───────────┘

┌────────────┼────────────┐
│ │ │
┌───────────┐┌───────────┐┌───────────┐
│ElementNode││ TextNode ││CommentNode│
└───────────┘└───────────┘└───────────┘

作为容器节点的ElementNode又可以添加任意个Node,这样就可以构成层级结构。

类似的,像文件夹和文件、GUI窗口的各种组件,都符合Composite模式的定义,因为它们的结构天生就是层级结构。