组合模式的核心思想是将单个对象和组合对象统一对待,让客户端可以用一致的方式处理整体与部分的层级结构,在Golang中通过接口定义统一的行为,再让叶子节点和组合节点分别实现该接口即可完成模式落地。

组合模式适用的数据结构类型
组合模式最适合处理具备天然层级嵌套特征的数据结构,常见的适用场景包括以下几类:
- 树形结构:比如公司组织架构、商品分类体系,这类结构存在明显的父节点和子节点关系,父节点可以包含多个子节点,子节点也可以是父节点继续向下嵌套。
- 文件系统结构:目录和文件的关系就是典型的组合模式场景,目录可以包含子目录和普通文件,普通文件是叶子节点没有子元素。
- UI组件树:前端或者桌面应用的UI组件通常是嵌套结构,容器组件可以包含多个子组件,子组件也可以是容器组件继续嵌套。
- 菜单导航结构:多级导航菜单中,父菜单可以包含子菜单和菜单项,菜单项是叶子节点无法再展开。
Golang中组合模式的实现核心
在Golang中实现组合模式,首先需要定义一个统一的接口,声明所有节点共有的方法,比如获取名称、添加子节点、删除子节点、展示内容等。然后分别实现两个结构体:一个是叶子节点结构体,实现接口的所有方法,其中添加、删除子节点的方法可以返回错误或者空实现;另一个是组合节点结构体,除了实现接口的基础方法,还需要维护一个子节点切片,用来存储下属的子节点。
统一接口定义
我们先定义组件的统一接口,所有节点都需要实现这个接口的方法:
// Component 定义组合模式的统一接口
type Component interface {
// GetName 获取节点名称
GetName() string
// Add 添加子节点,叶子节点可以不实现具体逻辑
Add(c Component) error
// Remove 移除子节点,叶子节点可以不实现具体逻辑
Remove(c Component) error
// Display 展示节点内容,叶子节点和组合节点的实现逻辑不同
Display(depth int)
}
叶子节点实现
叶子节点是没有子节点的结构,比如文件、菜单项,添加和删除子节点的方法可以返回错误提示:
// Leaf 叶子节点结构体
type Leaf struct {
name string
}
// NewLeaf 创建叶子节点实例
func NewLeaf(name string) *Leaf {
return &Leaf{name: name}
}
// GetName 实现获取名称方法
func (l *Leaf) GetName() string {
return l.name
}
// Add 叶子节点不支持添加子节点,返回错误
func (l *Leaf) Add(c Component) error {
return fmt.Errorf("叶子节点 %s 不支持添加子节点", l.name)
}
// Remove 叶子节点不支持移除子节点,返回错误
func (l *Leaf) Remove(c Component) error {
return fmt.Errorf("叶子节点 %s 不支持移除子节点", l.name)
}
// Display 展示叶子节点内容,根据深度添加缩进
func (l *Leaf) Display(depth int) {
for i := 0; i < depth; i++ {
fmt.Print("-")
}
fmt.Println("叶子节点:" + l.name)
}
组合节点实现
组合节点可以包含多个子节点,需要维护一个子节点切片,实现添加、删除、展示等方法:
// Composite 组合节点结构体
type Composite struct {
name string
children []Component
}
// NewComposite 创建组合节点实例
func NewComposite(name string) *Composite {
return &Composite{
name: name,
children: make([]Component, 0),
}
}
// GetName 实现获取名称方法
func (c *Composite) GetName() string {
return c.name
}
// Add 添加子节点到组合节点
func (c *Composite) Add(child Component) error {
c.children = append(c.children, child)
return nil
}
// Remove 从组合节点移除指定子节点
func (c *Composite) Remove(child Component) error {
for i, item := range c.children {
if item.GetName() == child.GetName() {
c.children = append(c.children[:i], c.children[i+1:]...)
return nil
}
}
return fmt.Errorf("子节点 %s 不存在于组合节点 %s 中", child.GetName(), c.name)
}
// Display 展示组合节点及其所有子节点内容
func (c *Composite) Display(depth int) {
for i := 0; i < depth; i++ {
fmt.Print("-")
}
fmt.Println("组合节点:" + c.name)
// 递归展示所有子节点,深度加1
for _, child := range c.children {
child.Display(depth + 2)
}
}
用组合模式建模文件系统层级关系
我们用上面的实现来模拟一个简单的文件系统,根目录是组合节点,下面可以包含子目录和普通文件,普通文件是叶子节点:
func main() {
// 创建根目录
root := NewComposite("根目录")
// 创建两个子目录
subDir1 := NewComposite("文档目录")
subDir2 := NewComposite("图片目录")
// 创建几个普通文件
file1 := NewLeaf("简历.pdf")
file2 := NewLeaf("报告.docx")
file3 := NewLeaf("风景.jpg")
file4 := NewLeaf("头像.png")
// 组装层级结构
subDir1.Add(file1)
subDir1.Add(file2)
subDir2.Add(file3)
subDir2.Add(file4)
root.Add(subDir1)
root.Add(subDir2)
// 展示整个文件系统的层级结构
fmt.Println("文件系统层级结构:")
root.Display(0)
// 测试移除子节点
subDir1.Remove(file1)
fmt.Println("n移除简历.pdf后的文档目录结构:")
subDir1.Display(2)
}
运行上面的代码,会先输出完整的文件系统层级,然后输出移除指定文件后的子目录结构,整个过程客户端不需要区分处理的是目录还是文件,都是用统一的Component接口调用方法,这就是组合模式的核心优势。
组合模式使用注意事项
虽然组合模式可以统一处理层级结构,但也有一些需要注意的点:首先叶子节点需要实现添加、删除子节点的方法,如果这些方法没有实际意义,返回错误是比较合理的处理方式;其次如果层级结构非常深,递归调用展示或者遍历的时候可能会出现栈溢出的问题,需要根据实际场景调整实现逻辑;最后如果不同节点的行为差异很大,强行用组合模式统一可能会导致接口方法过多,不符合单一职责原则,这时候需要评估是否适合使用该模式。