Menu
CoddyTech

组合模式

本课属于 Coddy PHP 学习旅程的“面向对象编程”单元。第 80 课,共 91 课。

组合模式是一种结构型设计模式,可以将对象组合成树形结构,并像处理单个对象一样处理这些结构。它非常适合表示整体与部分的层级关系,让你可以统一处理单个对象和对象组。

考虑一个文件系统。文件夹可以包含文件和其他文件夹。

计算总大小时,你需要对文件和文件夹调用同一个方法,并让文件夹递归地汇总其内容。组合模式让这一切变得简单,因为它为叶子(文件)和组合对象(文件夹)提供了相同的接口。

<?php
interface FileSystemItem {
    public function getName(): string;
    public function getSize(): int;
}

class File implements FileSystemItem {
    public function __construct(
        private string $name,
        private int $size
    ) {}
    
    public function getName(): string {
        return $this->name;
    }
    
    public function getSize(): int {
        return $this->size;
    }
}

class Folder implements FileSystemItem {
    private array $items = [];
    
    public function __construct(private string $name) {}
    
    public function getName(): string {
        return $this->name;
    }
    
    public function add(FileSystemItem $item): void {
        $this->items[] = $item;
    }
    
    public function getSize(): int {
        $total = 0;
        foreach ($this->items as $item) {
            $total += $item->getSize();
        }
        return $total;
    }
}

现在,你可以构建树形结构,并统一地处理它们:

<?php
$docs = new Folder("Documents");
$docs->add(new File("resume.pdf", 250));
$docs->add(new File("cover.docx", 50));

$images = new Folder("Images");
$images->add(new File("photo.jpg", 1500));

$root = new Folder("Root");
$root->add($docs);
$root->add($images);
$root->add(new File("readme.txt", 10));

echo $root->getSize();

输出:

1810

客户端代码调用 getSize(),无需关心它处理的是单个文件还是深层嵌套的文件夹结构。这个模式在内部处理递归,让复杂的层次结构也易于操作。

challenge icon

挑战

简单

让我们使用组合模式构建一个组织架构系统。公司采用层级结构,部门可以包含员工和其他子部门:这是一种完美的树形结构,你可以用统一的方式计算整个组织的总额。

你要将代码分到 4 个文件中:

  • OrganizationUnit.php:定义一个 OrganizationUnit 接口,由员工个人和部门共同实现。它应包含两个方法:getName(): string 和 getSalary(): int。这个通用接口让你在计算成本时,可以用相同的方式处理单个员工和整个部门。
  • Employee.php:引入 OrganizationUnit 接口,并创建一个实现该接口的 Employee 类。员工是组织树中的叶节点。他们有姓名和薪水,但不能包含其他单元。使用构造函数属性提升来接收姓名和薪水,并实现这两个接口方法。
  • Department.php:引入 OrganizationUnit 接口,并创建一个实现该接口的 Department 类。部门是一个组合,可以同时包含员工和其他部门。你的部门应当:
    • 在构造函数中接收一个名称
    • 实现 getName(),返回部门名称
    • 提供一个 add(OrganizationUnit $unit): void 方法,用来添加员工或子部门
    • 实现 getSalary(),返回其包含的所有单元的薪水总和(递归累加内部所有单元的薪水)
  • main.php:引入 Employee 和 Department 文件。你会收到一个输入:表示组织结构的 JSON 字符串。

    JSON 格式如下:

    {"Engineering": [{"name": "Alice", "salary": 80000}, {"name": "Bob", "salary": 75000}], "Sales": [{"name": "Carol", "salary": 65000}]}

    每个键都是部门名称,对应的值是一个数组,其中包含员工的姓名和薪水。

    创建一个名为 "Company" 的根 Department。对于 JSON 中的每个部门,创建一个 Department,添加该部门的员工,然后将该部门添加到 Company。最后,按以下格式输出公司的薪水总额:

    Total Salary: [amount]

组合模式的优点在于,对 Company 部门调用 getSalary() 会自动计算所有嵌套部门和员工的薪水总和:客户端代码无需了解内部结构。

亲手试试

<?php
require_once 'Employee.php';
require_once 'Department.php';

// 读取 JSON 输入
$input = trim(fgets(STDIN));
$data = (array)json_decode($input, true);

// TODO: 创建一个名为 "Company" 的根 Department 对象

// TODO: 遍历 JSON 数据中的每个部门
// 对于每个部门:
//   创建一个新的 Department 对象
//   将每位员工添加到该部门
//   将该部门添加到 Company

// TODO: 按以下格式输出工资总额:Total Salary: [amount]

?>
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: PHP 在线编译器