组合模式(Composite):让"单个"和"一群"用同一个接口
更新时间:2026-08-25。本文按"先看不用模式的困境 → 再用模式重构 → 类图对比分析"的结构,论述组合模式。可运行代码:demos/design-patterns/composite。
一、问题场景:树形结构,"叶子"与"容器"要一致对待
文件系统(文件/目录)、组织架构(员工/部门)、UI 组件树(按钮/面板)、渲染场景图(节点/组)。叶子与容器行为相似(都能展示、都能计数、都能遍历),但容器还包含子节点。
二、不使用模式:叶子容器分开处理,客户端 if 分支
cpp
// 计算总价:文件有大小,目录要递归
double totalPrice(Node* n) {
if (n->isDir()) { // 类型分支
double s = 0;
for (auto* c : n->children()) s += totalPrice(c);
return s;
} else {
return n->fileSize();
}
}
// 每加一种处理(重命名、统计、搜索)都要写一套 if(isDir)| 缺陷 | 说明 |
|---|---|
| 违反 OCP | 加操作 = 加一套 if(isDir) 分支 |
| 客户端承担树遍历 | 递归逻辑散落在每个调用方 |
| 违反 LoD | 客户端必须了解容器内部结构 |
| 处理逻辑重复 | 遍历代码在客户端反复写 |
不使用模式的类图

三、使用模式:叶子与容器实现同一接口,递归天然成立
组合模式定义统一组件接口(含"操作"与"管理子节点"方法),叶子实现操作(无子节点,管理方法为空/抛错),容器实现操作 = 遍历子节点递归委托。客户端只依赖组件接口,无需知道叶子还是容器。
cpp
// component.h
class Component { // 统一接口(叶子 + 容器)
public:
virtual double price() const = 0;
virtual void add(std::unique_ptr<Component> child) {} // 叶子默认空
virtual ~Component() = default;
};
class File : public Component { // 叶子
public:
explicit File(double p) : price_(p) {}
double price() const override { return price_; }
private:
double price_;
};
class Dir : public Component { // 容器
public:
void add(std::unique_ptr<Component> c) override { children_.push_back(std::move(c)); }
double price() const override {
double s = 0;
for (auto& c : children_) s += c->price(); // 递归:子节点可能是 File 或 Dir
return s;
}
private:
std::vector<std::unique_ptr<Component>> children_;
};
// 客户端:不分叶子容器
void show(const Component& c) { std::cout << c.price(); }使用模式的类图

四、类图对比与分析
| 维度 | 不使用(类型分支) | 使用组合 |
|---|---|---|
| 客户端逻辑 | if(isDir) 分支 + 手工递归 | 无差别调用 price() |
| 加操作 | 每个调用方加分支 | 组件接口加方法,叶子/容器各自实现 |
| 树遍历 | 客户端各写各的 | 容器内部递归委托 |
| LoD | 客户端深入容器内部 | 客户端只跟 Component 说话 |
| 结构成本 | 无 | 容器管理方法在叶子端是空实现 |
分析结论:组合让"树形结构的处理"对客户端透明——统一接口 + 容器递归委托,客户端永远只说"给我价格/画出来"。代价是叶子被迫实现(空)容器管理方法,接口略"胖"(可用默认空实现缓解)。
五、适用边界
| 适用 | 不适用 |
|---|---|
| 树形/递归结构(文件树、菜单、UI 树) | 结构是扁平的(组合是过度设计) |
| 客户端要无差别处理叶子与容器 | 叶子与容器操作差异极大(统一接口牵强) |
| 递归操作是常态 | 需要区分叶子/容器的强类型场景 |
与装饰器的关系:组合的容器也是 Component 的子类(兄弟关系);装饰器则是 Component 的包装者(父子关系)。两者都靠统一接口递归,组合聚焦"聚合",装饰器聚焦"增强"。
一句话总结
组合模式让叶子与容器实现同一接口、容器通过递归委托处理子节点,客户端对"单个或一群"无差别调用——树形结构的遍历与操作从客户端收进组件自身;代价是容器管理方法在叶子端为空实现,适合天然递归的树形领域。