组合模式(Composite)
前言
组合模式将对象组合成树形结构,使客户端能统一处理单个对象和组合对象。它在文件系统、UI 组件树、菜单系统等树形结构场景中应用广泛。本文将详细讲解组合模式的核心原理及实际应用。
一、核心概念
1.1 定义
将对象组合成树形结构以表示"部分-整体"的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
1.2 核心角色
| 角色 | 说明 |
|---|---|
| 组件接口(Component) | 定义叶子和组合的公共接口 |
| 叶子(Leaf) | 树的末端节点,无子节点 |
| 组合(Composite) | 有子节点的容器,管理子组件 |
二、代码实现
2.1 基础实现
php
<?php
// 组件接口
interface Component
{
public function operation(): string;
public function add(Component $component): void;
public function remove(Component $component): void;
public function getChild(int $index): ?Component;
}
// 叶子节点
class Leaf implements Component
{
public function __construct(private string $name) {}
public function operation(): string
{
return "Leaf({$this->name})";
}
public function add(Component $component): void
{
throw new RuntimeException("Cannot add to a leaf");
}
public function remove(Component $component): void
{
throw new RuntimeException("Cannot remove from a leaf");
}
public function getChild(int $index): ?Component
{
return null;
}
}
// 组合节点
class Composite implements Component
{
/** @var Component[] */
private array $children = [];
public function operation(): string
{
$results = array_map(fn($child) => $child->operation(), $this->children);
return "Composite[" . implode(", ", $results) . "]";
}
public function add(Component $component): void
{
$this->children[] = $component;
}
public function remove(Component $component): void
{
$index = array_search($component, $this->children, true);
if ($index !== false) {
array_splice($this->children, $index, 1);
}
}
public function getChild(int $index): ?Component
{
return $this->children[$index] ?? null;
}
}
// 使用:构建树形结构
$root = new Composite();
$branch1 = new Composite();
$branch2 = new Composite();
$branch1->add(new Leaf("A"));
$branch1->add(new Leaf("B"));
$branch2->add(new Leaf("C"));
$root->add($branch1);
$root->add($branch2);
$root->add(new Leaf("D"));
echo $root->operation() . PHP_EOL;
// Composite[Composite[Leaf(A), Leaf(B)], Composite[Leaf(C)], Leaf(D)]2.2 实际案例:文件系统
php
<?php
// 文件系统组件接口
interface FileSystemNode
{
public function getName(): string;
public function getSize(): int;
public function print(string $indent = ""): void;
}
// 文件(叶子节点)
class File implements FileSystemNode
{
public function __construct(
private string $name,
private int $size
) {}
public function getName(): string
{
return $this->name;
}
public function getSize(): int
{
return $this->size;
}
public function print(string $indent = ""): void
{
echo "{$indent}📄 {$this->name} ({$this->size}KB)" . PHP_EOL;
}
}
// 文件夹(组合节点)
class Folder implements FileSystemNode
{
/** @var FileSystemNode[] */
private array $children = [];
public function __construct(private string $name) {}
public function getName(): string
{
return $this->name;
}
public function getSize(): int
{
return array_sum(array_map(fn($child) => $child->getSize(), $this->children));
}
public function add(FileSystemNode $node): void
{
$this->children[] = $node;
}
public function remove(FileSystemNode $node): void
{
$index = array_search($node, $this->children, true);
if ($index !== false) {
array_splice($this->children, $index, 1);
}
}
public function print(string $indent = ""): void
{
echo "{$indent}📁 {$this->name}/ ({$this->getSize()}KB)" . PHP_EOL;
foreach ($this->children as $child) {
$child->print($indent . " ");
}
}
}
// 使用
$root = new Folder("project");
$src = new Folder("src");
$docs = new Folder("docs");
$src->add(new File("index.php", 5));
$src->add(new File("utils.php", 3));
$docs->add(new File("README.md", 2));
$root->add($src);
$root->add($docs);
$root->add(new File("composer.json", 1));
$root->print();
// 📁 project/ (11KB)
// 📁 src/ (8KB)
// 📄 index.php (5KB)
// 📄 utils.php (3KB)
// 📁 docs/ (2KB)
// 📄 README.md (2KB)
// 📄 composer.json (1KB)2.3 实际案例:UI 组件树
php
<?php
// UI 组件接口
interface UIComponent
{
public function render(): string;
}
// 按钮组件(叶子)
class Button implements UIComponent
{
public function __construct(private string $label) {}
public function render(): string
{
return "<button>{$this->label}</button>";
}
}
// 文本组件(叶子)
class Text implements UIComponent
{
public function __construct(private string $content) {}
public function render(): string
{
return "<span>{$this->content}</span>";
}
}
// 容器组件(组合)
class Container implements UIComponent
{
/** @var UIComponent[] */
private array $children = [];
public function __construct(private string $style = "") {}
public function addChild(UIComponent $component): void
{
$this->children[] = $component;
}
public function render(): string
{
$children = implode("\n ", array_map(fn($c) => $c->render(), $this->children));
return "<div style=\"{$this->style}\">\n {$children}\n</div>";
}
}
// 使用
$app = new Container("display: flex");
$header = new Container("padding: 10px");
$body = new Container("flex: 1");
$header->addChild(new Text("Welcome"));
$body->addChild(new Button("Click Me"));
$body->addChild(new Button("Cancel"));
$app->addChild($header);
$app->addChild($body);
echo $app->render() . PHP_EOL;三、适用场景
| 场景 | 说明 |
|---|---|
| 文件系统 | 文件和文件夹的树形结构 |
| UI 组件树 | 组件嵌套渲染 |
| 菜单系统 | 多级菜单展示 |
| 组织架构 | 公司部门树 |
| DOM 操作 | HTML 节点树 |
| 评论系统 | 嵌套评论 |
四、优缺点分析
| 优点 | 缺点 |
|---|---|
| 统一处理叶子和组合 | 叶子可能有冗余方法 |
| 符合开闭原则 | 设计可能过度 |
| 简化客户端代码 | 类型安全需额外处理 |
| 灵活构建树形结构 | 递归调用可能过深 |
五、常见踩坑与问题排查
5.1 叶子节点抛异常
php
<?php
// 问题:叶子节点调用 add/remove 抛异常
$leaf = new Leaf("A");
$leaf->add(new Leaf("B")); // 抛出异常
// 解决方案 1:空实现(安全但可能隐藏错误)
class SafeLeaf implements Component
{
public function operation(): string { return "SafeLeaf"; }
public function add(Component $component): void {} // 空实现
public function remove(Component $component): void {}
public function getChild(int $index): ?Component { return null; }
}
// 解决方案 2:接口分离
interface Component
{
public function operation(): string;
}
interface CompositeComponent extends Component
{
public function add(Component $c): void;
public function remove(Component $c): void;
}5.2 循环引用
php
<?php
// 问题:A 包含 B,B 包含 A,导致无限递归
$a = new Composite();
$b = new Composite();
$a->add($b);
$b->add($a); // 循环引用
// 解决:添加循环检测
class SafeComposite extends Composite
{
public function add(Component $component): void
{
if ($this->isAncestor($component)) {
throw new RuntimeException("Cannot add: circular reference detected");
}
parent::add($component);
}
private function isAncestor(Component $node): bool
{
// 检查是否形成循环
return false;
}
}六、优化方案与进阶
6.1 透明组合 vs 安全组合
php
<?php
// 透明组合:叶子也有 add/remove(空实现或抛异常)
// 安全组合:只有组合才有 add/remove(接口分离)
// 安全组合更推荐
interface Component
{
public function operation(): void;
}
interface CompositeInterface extends Component
{
public function add(Component $c): void;
public function remove(Component $c): void;
/** @return Component[] */
public function getChildren(): array;
}6.2 结合访问者模式
php
<?php
interface Visitor
{
public function visitFile(File $file): void;
public function visitFolder(Folder $folder): void;
}
class SizeCalculatorVisitor implements Visitor
{
public int $totalSize = 0;
public function visitFile(File $file): void
{
$this->totalSize += $file->getSize();
}
public function visitFolder(Folder $folder): void
{
// 递归访问子节点
}
}七、全文总结
组合模式的核心是 将对象组合成树形结构,统一处理单个对象和组合对象。
核心要点:
- 叶子和组合实现统一接口
- 递归构建树形结构
- 适用于文件系统、UI 组件、菜单等树形数据
- 注意叶子节点的方法处理(透明式 vs 安全式)
- 可与访问者模式结合,实现复杂的遍历操作
