🌳 一、引言:当对象呈树状生长时,管理就需要"组合"
在实际开发中,我们经常遇到需要处理层级结构的场景:
- 文件系统(文件夹中包含文件或子文件夹)
- UI 组件树(窗口 → 面板 → 按钮)
- 公司组织架构(部门 → 子部门 → 员工)
- 菜单系统(菜单 → 子菜单 → 菜单项)
这些结构都具有相同特点:
- 整体与部分需要统一处理
- 层级可能无限嵌套
- 不同类型节点应该具备一致的访问方式
这正是**组合模式(Composite Pattern)**发挥作用的地方。
组合模式的核心思想:
使用统一的组件接口,使"整体对象"和"部分对象"在结构上保持一致,从而让客户端可以以一致的方式操作层级结构。
🧩 二、核心概念:Component / Leaf / Composite
组合模式中包含三类角色:
| 角色 | 描述 |
|---|---|
| Component(抽象组件) | 定义公共接口,约束所有节点的行为 |
| Leaf(叶子节点) | 代表最小单元,不再包含子节点 |
| Composite(组合节点) | 可以包含子节点,也实现与 Component 相同的接口 |
Component +operation() Leaf +operation() Composite -children: List +add(Component) +remove(Component) +operation()
组合模式让客户端可以统一对待叶子节点和组合节点。
🗂️ 三、经典示例:文件系统的统一管理
以文件系统为例:
- 文件夹(Composite)可以包含文件或子文件夹
- 文件(Leaf)属于最基本单元
- 用户可以对文件或文件夹执行相同操作:如展示名称、统计大小、删除等
组合模式特别适合这种递归结构。
🧱 四、Java 示例:构建层级结构并统一操作
java
// 抽象组件
public abstract class FileComponent {
protected String name;
public FileComponent(String name) {
this.name = name;
}
public abstract void display();
public void add(FileComponent component) {
throw new UnsupportedOperationException();
}
public void remove(FileComponent component) {
throw new UnsupportedOperationException();
}
}
// 叶子节点
public class FileLeaf extends FileComponent {
public FileLeaf(String name) {
super(name);
}
@Override
public void display() {
System.out.println("📄 File: " + name);
}
}
// 组合节点
import java.util.ArrayList;
import java.util.List;
public class FolderComposite extends FileComponent {
private List<FileComponent> children = new ArrayList<>();
public FolderComposite(String name) {
super(name);
}
@Override
public void add(FileComponent component) {
children.add(component);
}
@Override
public void remove(FileComponent component) {
children.remove(component);
}
@Override
public void display() {
System.out.println("📁 Folder: " + name);
for (FileComponent c : children) {
c.display();
}
}
}
// 使用示例
public class Client {
public static void main(String[] args) {
FolderComposite root = new FolderComposite("root");
FolderComposite doc = new FolderComposite("doc");
FolderComposite img = new FolderComposite("img");
root.add(doc);
root.add(img);
doc.add(new FileLeaf("resume.docx"));
doc.add(new FileLeaf("design.pdf"));
img.add(new FileLeaf("logo.png"));
img.add(new FileLeaf("banner.jpg"));
root.display();
}
}
输出:
console
📁 Folder: root
📁 Folder: doc
📄 File: resume.docx
📄 File: design.pdf
📁 Folder: img
📄 File: logo.png
📄 File: banner.jpg
客户端完全无需区分文件夹和文件。
🧭 五、使用场景
组合模式适用于:
- 需要表示 树形结构
- 整体与部分需要一致处理
- 结构具有 递归性
- 操作需要对树中所有节点执行
- 操作需要对树中所有节点执行(如 render、delete、calculate)
典型例子:
- DOM 结构
- 菜单系统
- 公司组织架构
- 游戏场景树(Unity、UE)
🔁 六、统一操作如何实现?
客户端只需调用:
java
component.operation();
无论 component 是:
- 一个简单对象(Leaf)
- 一个树结构(Composite)
- 甚至是一个空节点
它都遵循相同接口,这便带来了极大的灵活性。
在这种设计中,有两种方式处理 add/remove:
① 透明方式
Component 中声明 add/remove(不管是否叶子)
优点:客户端完全一致操作
缺点:叶子节点的方法会无意义
② 安全方式
只有 Composite 提供 add/remove
优点:语义清晰
缺点:客户端需判断是否为 Composite
组合模式根据需要选择透明或安全方式。
🔍 七、与其他模式的关系
组合模式常与以下模式一起出现:
✔ 1. 迭代器模式(Iterator)
用于遍历树形结构。
✔ 2. 责任链模式(Chain of Responsibility)
节点层级可向父节点传递消息。
✔ 3. 访问者模式(Visitor)
对树中所有节点施加新的操作。
✔ 4. 享元模式(Flyweight)
某些叶子节点可被共享,减少内存。
组合模式是构建复杂结构体系的"基础骨架"。
🛠️ 八、实现技巧
1. 孩子列表可延迟初始化
减少内存开销:
java
private List<FileComponent> children; // 需要时才 new
2. Composite 必须实现所有 Component 方法
确保调用一致性。
3. operation 内可决定是否遍历子节点
如 UI 渲染中可根据 visible 属性优化。
4. 通常配合多态实现递归
递归是组合模式的灵魂。
🎮 九、实战:游戏场景树
例如 UI 控件:
- 叶子(Button)可能有 click 动作
- 容器(Panel)可能有 layout 布局
此时需要:
- 共享接口作为 Component
- 各自扩展行为不在 Component 中声明
组合模式并不要求所有方法一致,只是要求核心功能一致。
📐 十、复杂 UI 渲染结构
Root Node Layout Node Style Node Render Node Children Layout Children Render
父节点负责:
- 计算布局
- 应用样式
- 触发渲染
子节点生成子图层,最终合成一棵可渲染树。
🧲 十一、批量操作示例
在树中执行批处理,例如:
- 统计所有节点数量
- 查找节点
- 删除节点
- 批量刷新
操作基本套路:
java
void operation() {
// 当前节点的操作
...
// 递归对子节点操作
for (Component c : children) {
c.operation();
}
}
组合模式的"统一入口"设计让递归操作非常自然。
📐 十二、优缺点
优点
- 统一处理整体与部分
- 扩展性强
- 递归结构更易管理
- 操作可作用于整棵树
缺点
- 系统结构可能过于通用
- 叶子暴露无意义方法
- 调试复杂(深层递归)
🎯 十三、总结
组合模式让我们能够:
- 自然地处理树结构
- 统一管理对象层级
- 对整个结构执行批处理操作
是 UI、游戏引擎、组织架构、文件系统等领域的核心模式。