组合模式
本课属于 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。
挑战
简单让我们使用组合模式构建一个组织结构图系统。你将创建一个层级结构,其中单个员工和部门(部门中可以包含其他员工或子部门)都能以统一的方式处理。这与真实公司的组织方式相似:部门中包含员工和其他部门,形成树形结构。
你将把代码分到三个文件中:
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:构建并显示组织结构。读取四项输入:
- 公司名称(字符串)
- 部门名称(字符串)
- 第一位员工的姓名和薪资(格式:
name,salary) - 第二位员工的姓名和薪资(格式:
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;
}
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
自己练习: C++ 在线编译器