Skip to content

组合模式(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
    {
        // 递归访问子节点
    }
}

七、全文总结

组合模式的核心是 将对象组合成树形结构,统一处理单个对象和组合对象

核心要点

  1. 叶子和组合实现统一接口
  2. 递归构建树形结构
  3. 适用于文件系统、UI 组件、菜单等树形数据
  4. 注意叶子节点的方法处理(透明式 vs 安全式)
  5. 可与访问者模式结合,实现复杂的遍历操作