Menu
CoddyTech

组合模式

本课属于 Coddy C++ 学习旅程的“面向对象编程”单元。第 101 课,共 104 课。

组合模式让你可以统一处理单个对象和对象组。它将对象组合成树形结构,其中单个元素和元素容器共享相同的接口。这非常适合表示文件系统、组织结构图或 UI 组件等层级结构。

这种模式有三个关键部分:定义通用操作的 Component 接口、表示单个对象的 Leaf 类,以及包含子对象并将操作委托给它们的 Composite 类:

#include <iostream>
#include <memory>
#include <vector>
#include <string>

// 组件接口
class FileSystemItem {
public:
    virtual void display(int indent = 0) const = 0;
    virtual int getSize() const = 0;
    virtual ~FileSystemItem() = default;
};

// 叶子节点:表示单个文件
class File : public FileSystemItem {
    std::string name;
    int size;
public:
    File(const std::string& n, int s) : name(n), size(s) {}
    
    void display(int indent = 0) const override {
        std::cout << std::string(indent, ' ') << name 
                  << " (" << size << " KB)\n";
    }
    int getSize() const override { return size; }
};

// 组合节点:包含其他组件
class Folder : public FileSystemItem {
    std::string name;
    std::vector<std::shared_ptr<FileSystemItem>> children;
public:
    Folder(const std::string& n) : name(n) {}
    
    void add(std::shared_ptr<FileSystemItem> item) {
        children.push_back(item);
    }
    
    void display(int indent = 0) const override {
        std::cout << std::string(indent, ' ') << "[" << name << "]\n";
        for (const auto& child : children) {
            child->display(indent + 2);
        }
    }
    
    int getSize() const override {
        int total = 0;
        for (const auto& child : children) {
            total += child->getSize();
        }
        return total;
    }
};

Folder 组合对象保存子项,并通过遍历子项来实现操作。在文件夹上调用 getSize() 时,它会递归计算所有所含项目的总大小。客户端代码不需要知道它处理的是文件还是文件夹,因为两者都响应相同的接口。

当你需要表示部分与整体的层次结构,并希望客户端统一处理单个对象和组合对象时,可以使用 Composite。

challenge icon

挑战

简单

让我们使用组合模式构建一个组织结构图系统。你将创建一个层级结构,其中单个员工和部门(部门中可以包含其他员工或子部门)都能以统一的方式处理。这与真实公司的组织方式相似:部门中包含员工和其他部门,形成树形结构。

你将把代码分到三个文件中:

  • OrgComponent.h:定义员工和部门都会实现的组件接口。

    创建一个抽象类 OrgComponent,包含:

    • getName():返回组件的名称
    • getSalary():返回总薪资(员工返回自己的薪资,部门返回所有成员薪资的总和)
    • display(int indent = 0):以正确的缩进显示组件

    添加一个虚析构函数。

  • Organization.h:实现叶子类和组合类。

    创建一个 Employee 类(叶子),用于存储姓名和薪资。它的 display() 方法应按以下格式输出员工信息:

    [indent spaces]- [name] ($[salary])

    创建一个 Department 类(组合),用于存储名称和一个通过 std::shared_ptr 管理的 OrgComponent 子组件集合。实现以下方法:

    • add(std::shared_ptr<OrgComponent> component):向部门添加一个子组件
    • getSalary():递归计算所有成员的总薪资
    • display():将部门名称放在方括号中输出,然后以增加的缩进显示所有子组件(每层增加 2 个空格)

    部门的显示格式应为:

    [indent spaces][Department Name]
      [children displayed with indent + 2]
  • main.cpp:构建并显示组织结构。

    读取四项输入:

    1. 公司名称(字符串)
    2. 部门名称(字符串)
    3. 第一位员工的姓名和薪资(格式:name,salary)
    4. 第二位员工的姓名和薪资(格式:name,salary)

    构建以下结构:创建一家公司(顶层部门),向其中添加一个子部门,再将两位员工都添加到该子部门。然后显示整个组织结构,并输出公司的总薪资。

    显示结构后,输出:

    Total Salary: $[amount]

例如,输入 TechCorp、Engineering、Alice,75000 和 Bob,65000 时:

[TechCorp]
  [Engineering]
    - Alice ($75000)
    - Bob ($65000)
Total Salary: $140000

输入 StartupInc、Development、Carol,80000 和 Dave,70000 时:

[StartupInc]
  [Development]
    - Carol ($80000)
    - Dave ($70000)
Total Salary: $150000

注意,无论在员工还是部门上调用 getSalary(),它的工作方式都是一致的:部门会自动汇总所有成员的薪资。计算总额或显示层级结构时,客户端代码无需区分单个员工和整个部门。

亲手试试

#include <iostream>
#include <string>
#include <memory>
#include <sstream>
#include "Organization.h"

int main() {
    // 读取输入
    std::string companyName;
    std::string departmentName;
    std::string employee1Input;
    std::string employee2Input;
    
    std::getline(std::cin, companyName);
    std::getline(std::cin, departmentName);
    std::getline(std::cin, employee1Input);
    std::getline(std::cin, employee2Input);
    
    // 用于解析 "name,salary" 格式的辅助 lambda 函数
    auto parseEmployee = [](const std::string& input) -> std::pair<std::string, int> {
        size_t commaPos = input.find(',');
        std::string name = input.substr(0, commaPos);
        int salary = std::stoi(input.substr(commaPos + 1));
        return {name, salary};
    };
    
    auto [name1, salary1] = parseEmployee(employee1Input);
    auto [name2, salary2] = parseEmployee(employee2Input);
    
    // TODO: 创建公司,将其设为顶层 Department
    
    // TODO: 创建一个子部门
    
    // TODO: 使用解析后的数据创建两个 Employee 对象
    
    // TODO: 将员工添加到子部门
    
    // TODO: 将子部门添加到公司
    
    // TODO: 显示整个组织结构
    
    // TODO: 按以下格式输出工资总额:Total Salary: $[amount]
    
    return 0;
}
quiz icon自我测试

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

面向对象编程中的所有课

自己练习: C++ 在线编译器