Composite Design Pattern

组合模式(Composite Design Pattern)是一种结构型设计模式。意图通过构建树状数据结构并提供统一的处理方式来解决递归难题。 树状结构中的每个单个对象都可以以相同的方式处理,而不管它们是的复合的还是原子的。

组合模式的设计思路,与其说是一种设计模式,倒不如说是对业务场景的一种数据结构和算法的抽象。其中,数据可以表示成树这种数据结构,业务需求可以通过在树上的递归遍历算法来实现。 应用场景虽然单一,但是一旦用上就帮了兄弟大忙。

需要注意的是,管理树状数据结构的过程中,一定要有完备的检查机制,避免数据结构成环。同时需要思考的是,关键的计算型接口在计算环节如何提升性能。

UML Diagram

image

有没有发现这个结构图与修饰器模式的结构图几乎一模一样?这就是为什么说 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