Composite Design Pattern
组合模式(Composite Design Pattern)是一种结构型设计模式。意图通过构建树状数据结构并提供统一的处理方式来解决递归难题。
树状结构中的每个单个对象都可以以相同的方式处理,而不管它们是的复合的还是原子的。
组合模式的设计思路,与其说是一种设计模式,倒不如说是对业务场景的一种数据结构和算法的抽象。其中,数据可以表示成树这种数据结构,业务需求可以通过在树上的递归遍历算法来实现。
应用场景虽然单一,但是一旦用上就帮了兄弟大忙。
需要注意的是,管理树状数据结构的过程中,一定要有完备的检查机制,避免数据结构成环。同时需要思考的是,关键的计算型接口在计算环节如何提升性能。
UML Diagram
- Client 为主调方,依赖 Component 接口;
- Component 表示对结构树中每个节点的接口定义;
- Leaf 表示内嵌结构,结构树中的叶子节点;
- Composite 表示组合结构,非叶子节点的“抽象类”:
- 组合结构必须内嵌一组实现了 Component 接口的实例;
- 修饰器本身必须实现 Component 接口;
- Concrete Composite 为遵循 Composite 约束的组合结构;
有没有发现这个结构图与修饰器模式的结构图几乎一模一样?这就是为什么说 Go 去实现设计模式简单直接。
Example
Question:
设计一个文件目录树管理模块,提供以下操作界面:
1. 以某个文件路径获取一棵文件树;
2. 统计某棵文件树的文件个数;
3. 统计某棵文件树的磁盘占用总和;
Code:
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
|
package main
import (
"fmt"
"os"
)
const BasePath = "./"
// 文件树节点接口
type fileNode interface {
getPath() string
getNum() int
getSize() int64
}
// 叶子节点结构,它实现 fileNode
type file struct {
path string
}
func newFile(path string) *file {
return &file{path: path}
}
func (f *file) getPath() string {
return f.path
}
func (f *file) getNum() int {
return 1
}
func (f *file) getSize() int64 {
fi, err := os.Stat(f.getPath())
if err != nil {
return 0
}
return fi.Size()
}
// 目录节点结构,它内嵌一组 fileNode 同时实现 fileNode
type Directory struct {
components []fileNode
path string
}
func newDirectory(path string) *Directory {
return &Directory{path: path}
}
func (f *Directory) getPath() string {
return f.path
}
// 将原本的递归求解,转换成循环求解
func (f *Directory) getNum() int {
var sum int
for _, v := range f.components {
sum += v.getNum()
}
return sum
}
// 将原本的递归求解,转换成循环求解
func (f *Directory) getSize() int64 {
var size int64
for _, v := range f.components {
size += v.getSize()
}
return size
}
// 目录节点需提供对子节点的管理
// 缺少检查机制
func (f *Directory) addSubNode(node ...fileNode) {
f.components = append(f.components, node...)
}
func main() {
fileSystemTree := newDirectory(BasePath)
dirMusic := newDirectory(BasePath + "music/")
dirPost := newDirectory(BasePath + "post/")
fileSystemTree.addSubNode(dirMusic)
fileSystemTree.addSubNode(dirPost)
fileMusic1 := newFile(BasePath + "music/1.txt")
fileMusic2 := newFile(BasePath + "music/2.txt")
dirMusic.addSubNode(fileMusic1, fileMusic2)
filePost1 := newFile(BasePath + "post/1.txt")
dirPost.addSubNode(filePost1)
fmt.Println("/ file num:", fileSystemTree.getNum())
fmt.Println("/ file size:", fileSystemTree.getSize())
}
|
Output:
/ file num: 3
/ file size: 39
Refactor:
目前,每次调用 getNum() 和 getSize() 时都需要重新遍历一遍子树。如何提升它们的效率?
这个问题本质是如何优化递归算法的重复计算问题。
可以用散列表存储每个 <path,num> 和 <path,size>,通过路径直接返回对应的 size。
与此同时,在节点关系维护(删除或者添加自节点)的接口上同时对这些 map 进行维护。
Application Scenarios
References
https://time.geekbang.org/column/article/207456
https://golangbyexample.com/composite-design-pattern-golang