组合模式有哪些角色?调用流程是什么?
简化版
组合模式通常有三个角色:Component 定义统一组件接口,Leaf 表示叶子节点,Composite 表示组合节点并持有子组件。调用流程是客户端调用 Component 接口,如果是 Leaf 就直接执行,如果是 Composite 就递归调用它的子组件。
详细版
组合模式角色:
| 角色 | 作用 |
|---|---|
| Component | 抽象组件,定义叶子和组合对象的公共接口 |
| Leaf | 叶子节点,没有子节点 |
| Composite | 组合节点,可以包含多个 Component |
| Client | 通过 Component 接口统一操作对象树 |
调用流程:
Client 调用 root.operation()
root 如果是 Leaf:直接执行
root 如果是 Composite:遍历 children
children 中每个节点继续 operation()
组合模式的关键是 Composite 的子节点类型仍然是 Component,因此子节点可以是叶子,也可以是另一个组合节点。
完整版教学
一、Component:统一抽象
Component 定义公共能力:
interface MenuComponent {
void print();
}
客户端只依赖这个接口。
不管传进来的是菜单项还是菜单分组,都能调用 print()。
二、Leaf:叶子节点
叶子节点没有子节点。
class MenuItem implements MenuComponent {
private final String name;
MenuItem(String name) {
this.name = name;
}
public void print() {
System.out.println(name);
}
}
叶子节点只处理自己的行为。
三、Composite:组合节点
组合节点持有子组件:
class MenuGroup implements MenuComponent {
private final String name;
private final List<MenuComponent> children = new ArrayList<>();
void add(MenuComponent child) {
children.add(child);
}
public void print() {
System.out.println(name);
for (MenuComponent child : children) {
child.print();
}
}
}
这里 children 的类型是 MenuComponent,所以可以添加 MenuItem,也可以添加另一个 MenuGroup。
四、Client:统一调用
客户端代码:
MenuComponent root = buildMenu();
root.print();
客户端不需要判断 root 是叶子还是组合。
这就是组合模式最典型的使用方式。
五、递归调用是组合模式的自然结果
如果 MenuGroup 下面还有 MenuGroup,print() 会继续向下调用。
系统管理
用户管理
权限管理
角色管理
菜单管理
每一层都按同一个接口处理,递归结构非常自然。
六、常见误区与追问
组合模式的调用流体现递归多态。客户端持有1个 Component 引用,调用操作后,若对象是 Leaf 就直接完成;若是 Composite,则遍历3个子节点并对每个再次调用同一接口。客户端无需知道树的具体深度,但 Composite 必须维护子节点结构。
| 检查维度 | 判定依据 |
|---|---|
| Leaf | 执行基本操作并终止递归 |
| Composite | 将同一操作传播给子节点 |
记忆钩子:Component 统一入口,Leaf 收住递归,Composite 继续递归。
- 误区:Client 必须知道节点是叶子还是容器。 统一操作的目的正是消除客户端的类型判断。
- 追问:Composite 能否包含另一个 Composite? 可以,这种递归包含关系构成任意深度的树。
- 误区:Leaf 什么都不做。 它实现真正的基本业务操作,只是不管理子节点。
- 追问:谁负责创建整棵树? 客户端、构建器或仓储都可以,组合模式主要约束使用结构。
- 追问:怎样验证多态调用? 混合放入 Leaf 和 Composite,调用根接口并检查所有预期节点均被访问。
七、加强记忆
组合模式角色记住三个:Component 统一接口,Leaf 表示叶子,Composite 保存子节点并递归调用。客户端只面对 Component,所以能用同一套代码处理单个对象和整棵树。