大家好,我是十三!欢迎来到十三Tech。
前阵子做一个权限系统——组织架构是树形的,部门包含子部门、子部门包含员工。要算"某个部门的总人数"时,遇到部门就递归子节点、遇到员工就 +1。最初的代码写得很难看——一堆 if isDepartment { ... } else { ... },每加一种节点类型就要改一遍遍历逻辑。
后来把"员工"和"部门"统一抽象成 Node 接口,都实现 Headcount() 方法——员工返回 1,部门返回所有子节点的 Headcount() 之和。递归逻辑消失了,新增节点类型零修改调用方代码。这就是组合模式。
很多人把组合讲成"树形结构统一处理",但树形结构有十几种实现——访问者模式、迭代器模式、显式类型 switch 都能处理。这一篇就聊聊组合模式的标志特征是"叶子和容器同接口",以及它跟装饰器的微妙差别。
一、组合在解决什么——不是"树形结构"
教科书定义:将对象组合成树形结构以表示"部分-整体"的层次结构。这话模糊——任何树形数据都能套上。真正的组合有一个标志特征——叶子和容器实现同一个接口。
组合解决的核心问题是调用方被迫区分叶子和容器。没有组合模式时,遍历组织架构要写 if node is Department { for child := range node.Children { ... } } else { return 1 }——调用方知道太多内部细节,每加一种节点类型都要改调用方。
有了组合模式后,叶子和容器实现同一个 Node 接口。调用方拿到一个 Node,不需要也不应该知道它是叶子还是容器——调它的方法,递归在容器内部自动发生。
判断口诀:树形结构 + 叶子容器语义相近 → 组合;树形结构 + 叶子容器语义差异巨大 → 别用组合,老老实实类型 switch。
二、组合怎么写——同接口 + 容器内部递归
组合模式的核心是"叶子和容器同接口"。容器内部持有子节点列表,方法实现里递归调用所有子节点的同名方法。
type Node interface { Headcount() int }
type Employee struct{ name string }
func (e *Employee) Headcount() int { return 1 }
type Department struct {
name string
children []Node
}
func (d *Department) Headcount() int {
total := 0
for _, c := range d.children { total += c.Headcount() }
return total
}
看 Department.Headcount()——它不关心 children 里是 *Employee 还是 *Department,统一调 Node.Headcount()。如果是员工返回 1,如果是部门返回该部门的累计。递归在 Department 内部自动发生,调用方完全无感。
组合模式有个工程争议——Add/Remove 方法放哪儿。如果放 Node 接口,叶子节点也得实现(默认空或 panic);如果只放 Department,调用方要做类型断言才能加子节点。Go 工程上推荐放接口加默认实现——叶子节点 Add 直接 panic 或 no-op,调用方代码最干净。
三、什么时候别用组合
组合不是所有"树形数据"场景都适用,至少有三个反模式。
反模式一:叶子和容器语义差异巨大。比如文件系统里,文件可以"打开读取内容"、文件夹不能;文件夹可以"列出子项"、文件不能。强行合并接口会让接口臃肿(一堆方法在叶子或容器上无意义)。这种场景应该用访问者模式——访问者主动区分类型。
反模式二:树形结构很简单(2-3 种节点)。一个 Node 接口、一个 Leaf 实现、一个 Composite 实现共三个类型,写起来比直接 struct 嵌套更啰嗦。组合模式只有在节点类型多、操作多时才摊得平成本。
反模式三:用组合表示非树形数据。组合依赖递归语义——父节点调所有子节点的同名方法。如果你的数据是图(有交叉引用)或网(有多重父子关系),递归会陷入死循环。组合只适合严格的树形。
Go 标准库里组合最经典的地方是 go/ast.Node——Go 语言的 AST(抽象语法树)。每一段代码——FuncDecl(函数声明)、Expr(表达式)、Stmt(语句)、BlockStmt(语句块)——都实现 Node 接口。ast.Walk(visitor, node) 可以递归遍历整棵 AST,访问者不需要也不应该区分当前节点是函数声明还是表达式。看 go/format、go/imports 这些工具的源码,组合模式无处不在。
回头看那个权限系统——值得用组合吗?值得,组织架构是严格树形、叶子和容器语义相近(都要算人数)、未来可能新增节点类型(虚拟团队、矩阵组织),正是组合的甜区。但如果只是两三种节点的简单结构,直接 struct 嵌套更清晰。
组合真正教会我的,不是"树形结构统一处理",而是让调用方知道得越少越好——调用方只依赖 Node 接口,不感知具体类型。这个"接口隔离"的原则比模式本身重要得多。
下一篇进入行为型模式,从策略模式开始——讨论"对象之间怎么通信、怎么分配职责"。
关于十三Tech 资深服务端研发,AI 实践者,专注分享真实可落地的技术经验。 相信 AI 是程序员的最佳搭档。
联系方式:569893882@qq.com GitHub:@TriTechAI
