组合模式针对于特定场景,如文件管理、组织管理等,使用该模式能简化管理,使代码变得非常简洁。
组合模式:将对象组合成树形结构以表示‘部分-整体’的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
UML:
分析
单看UML图可能不清晰,举个栗子会容易一些。Composite是目录,Leaf是目录下的文件,目录和文件都继承自Component。目录能够增加、删除文件,可以展示目录所在位置,文件只能展示文件所在位置。
对于目录这种需求,有两种实现方式。
第一种不使用组合模式,只用一个类,有2个核心变量
- 一个成员变量表明对象是文件还是目录
- 一个成员变量存放目录下文件列表,如果对象为文件,则该变量为空
type FileSystemNode struct {
isFile bool //表明是文件还是目录
subNodes []FileSystemNode //目录下包含的内容
}
第二种方案使用组合模式。虽然第一种方案能够实现文件管理的功能,但并不优雅。因为文件和目录是不同的,各自有各自的特性,将特有的内容放到一个类里,不满足单一职责原则。
所以我们可以将其拆分为两个类:文件类和目录类。两个类必须继承自同一个父类,除了重复的功能可以复用外,更重要的一点是消除了两个类调用上的区别,subNodes不需要做任何区分。而且这两个类可以独立进化,相互不影响,何乐而不为呢。
使用场景
组合模式在使用上,特别像深度优先遍历或者广度优先遍历,一般用于组织结构、文件管理上,这些功能都有共通点:个体和集体无论在功能上还是认知上都极为相似。
代码实现
这次代码简单实现一下目录和文件的添加、显示功能吧。
package main
import "fmt"
const Separator = "--"
/**
* @Description: 文件系统接口,文件和目录都要实现该接口
*/
type FileSystemNode interface {
Display(separator string)
}
/**
* @Description: 文件通用功能
*/
type FileCommonFunc struct {
fileName string
}
/**
* @Description: 设置文件名称
* @receiver f
* @param fileName
*/
func (f *FileCommonFunc) SetFileName(fileName string) {
f.fileName = fileName
}
/**
* @Description: 文件类
*/
type FileNode struct {
FileCommonFunc
}
/**
* @Description: 文件类显示文件内容
* @receiver f
*/
func (f *FileNode) Display(separator string) {
fmt.Println(separator + f.fileName + " 文件内容为:Hello,world")
}
/**
* @Description: 目录类
*/
type DirectoryNode struct {
FileCommonFunc
nodes []FileSystemNode
}
/**
* @Description: 目录类展示文件名
* @receiver d
*/
func (d *DirectoryNode) Display(separator string) {
fmt.Println(separator + d.fileName)
for _, node := range d.nodes {
node.Display(separator + Separator)
}
}
/**
* @Description: 添加目录或者文件
* @receiver d
* @param f
*/
func (d *DirectoryNode) Add(f FileSystemNode) {
d.nodes = append(d.nodes, f)
}
func main() {
//初始化
biji := DirectoryNode{}
biji.SetFileName("笔记")
huiyi := DirectoryNode{}
huiyi.SetFileName("会议")
chenhui := FileNode{}
chenhui.SetFileName("晨会.md")
zhouhui := FileNode{}
zhouhui.SetFileName("周会.md")
//组装
biji.Add(&huiyi)
huiyi.Add(&chenhui)
huiyi.Add(&zhouhui)
//显示
biji.Display(Separator)
}
➜ myproject go run main.go
–笔记
—-会议
——晨会.md 文件内容为:Hello,world
——周会.md 文件内容为:Hello,world
文件类和目录类都实现了FileSystemNode接口,所以目录类管理文件类如同管理自己一样。两者都组合了FileCommonFunc类,可以复用相同功能。最后就是两者可以独立变化,如目录类有Add功能,但文件类没有。
实例
公司的人员组织就是一个典型的树状的结构,现在假设我们现在有部分,和员工,两种角色,一个部门下面可以存在子部门和员工,员工下面不能再包含其他节点。
我们现在要实现一个统计一个部门下员工数量的功能
代码
package composite
// IOrganization 组织接口,都实现统计人数的功能
type IOrganization interface {
Count() int
}
// Employee 员工
type Employee struct {
Name string
}
// Count 人数统计
func (Employee) Count() int {
return 1
}
// Department 部门
type Department struct {
Name string
SubOrganizations []IOrganization
}
// Count 人数统计
func (d Department) Count() int {
c := 0
for _, org := range d.SubOrganizations {
c += org.Count()
}
return c
}
// AddSub 添加子节点
func (d *Department) AddSub(org IOrganization) {
d.SubOrganizations = append(d.SubOrganizations, org)
}
// NewOrganization 构建组织架构 demo
func NewOrganization() IOrganization {
root := &Department{Name: "root"}
for i := 0; i < 10; i++ {
root.AddSub(&Employee{})
root.AddSub(&Department{Name: "sub", SubOrganizations: []IOrganization{&Employee{}}})
}
return root
}
单元测试
package composite
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewOrganization(t *testing.T) {
got := NewOrganization().Count()
assert.Equal(t, 20, got)
}
总结
组合模式是对指定场景有用,所以大家能不能用到,完全看运气。这个设计模式满足单一职责原则、开闭原则、里氏替换原则。