组合模式(Composite Pattern)是一种结构型设计模式,它允许你将对象组合成树形结构,并用统一的方式处理单个对象和对象组合。这种模式能让客户端以一致的方式对待单个对象和由多个对象组成的复合对象。
组合模式的核心角色
- Component(抽象组件):定义了叶子节点和复合节点的共同接口
- Leaf(叶子节点):表示树形结构中的单个对象,没有子节点
- Composite(复合节点):表示包含子节点的复合对象,实现了对其子节点的管理
C++实现示例
以下以文件系统为例实现组合模式,文件系统由文件(叶子节点)和文件夹(复合节点)组成,两者可以被统一处理:
cpp
#include <iostream>
#include <string>
#include <vector>
#include <memory>
// 抽象组件:文件系统组件
class FileSystemComponent {
protected:
std::string name;
public:
FileSystemComponent(std::string n) : name(std::move(n)) {}
virtual ~FileSystemComponent() = default;
// 获取名称
const std::string& getName() const { return name; }
// 纯虚方法:显示组件信息
virtual void display(int depth = 0) const = 0;
// 以下方法在叶子节点中默认不实现
virtual void add(std::unique_ptr<FileSystemComponent> component) {
throw std::runtime_error("不支持添加操作");
}
virtual void remove(const std::string& componentName) {
throw std::runtime_error("不支持删除操作");
}
virtual FileSystemComponent* getChild(const std::string& componentName) {
throw std::runtime_error("不支持获取子组件操作");
}
};
// 叶子节点:文件
class File : public FileSystemComponent {
private:
std::string content; // 文件内容
size_t size; // 文件大小(KB)
public:
File(std::string name, std::string cont, size_t sz)
: FileSystemComponent(std::move(name)), content(std::move(cont)), size(sz) {}
// 显示文件信息
void display(int depth = 0) const override {
std::string indent(depth, ' ');
std::cout << indent << "- 文件: " << name
<< " (" << size << "KB)" << std::endl;
}
// 文件特有方法:读取内容
std::string read() const {
return content;
}
};
// 复合节点:文件夹
class Folder : public FileSystemComponent {
private:
// 存储子组件(可以是文件或文件夹)
std::vector<std::unique_ptr<FileSystemComponent>> children;
public:
Folder(std::string name) : FileSystemComponent(std::move(name)) {}
// 添加子组件
void add(std::unique_ptr<FileSystemComponent> component) override {
children.push_back(std::move(component));
}
// 删除子组件
void remove(const std::string& componentName) override {
for (auto it = children.begin(); it != children.end(); ++it) {
if ((*it)->getName() == componentName) {
children.erase(it);
std::cout << "已删除: " << componentName << std::endl;
return;
}
}
throw std::runtime_error("未找到组件: " + componentName);
}
// 获取子组件
FileSystemComponent* getChild(const std::string& componentName) override {
for (const auto& child : children) {
if (child->getName() == componentName) {
return child.get();
}
}
return nullptr;
}
// 显示文件夹及其包含的所有子组件
void display(int depth = 0) const override {
std::string indent(depth, ' ');
std::cout << indent << "+ 文件夹: " << name << std::endl;
// 递归显示所有子组件,增加缩进
for (const auto& child : children) {
child->display(depth + 4);
}
}
// 文件夹特有方法:计算总大小
size_t getTotalSize() const {
size_t total = 0;
for (const auto& child : children) {
// 检查是否是文件
if (const File* file = dynamic_cast<const File*>(child.get())) {
total += file->read().size() / 1024; // 简化计算
}
// 检查是否是文件夹,递归计算
else if (const Folder* folder = dynamic_cast<const Folder*>(child.get())) {
total += folder->getTotalSize();
}
}
return total;
}
};
// 客户端代码:统一处理文件和文件夹
void processComponent(const FileSystemComponent* component) {
std::cout << "\n处理组件: " << component->getName() << std::endl;
component->display(2);
// 尝试获取总大小(仅文件夹支持)
if (const Folder* folder = dynamic_cast<const Folder*>(component)) {
std::cout << " 总大小: " << folder->getTotalSize() << "KB" << std::endl;
}
}
int main() {
// 创建文件
auto file1 = std::make_unique<File>("readme.txt", "这是一个说明文件", 2);
auto file2 = std::make_unique<File>("data.csv", "ID,Name,Value\n1,A,100", 5);
auto file3 = std::make_unique<File>("image.png", "[二进制图像数据]", 1024);
// 创建子文件夹
auto subFolder = std::make_unique<Folder>("文档");
subFolder->add(std::move(file1));
subFolder->add(std::move(file2));
// 创建根文件夹
auto rootFolder = std::make_unique<Folder>("我的文件");
rootFolder->add(std::move(subFolder));
rootFolder->add(std::move(file3));
// 显示整个文件系统结构
std::cout << "文件系统结构:" << std::endl;
rootFolder->display();
// 统一处理单个文件和文件夹
FileSystemComponent* imageFile = rootFolder->getChild("image.png");
processComponent(imageFile);
FileSystemComponent* docFolder = rootFolder->getChild("文档");
processComponent(docFolder);
// 测试删除操作
std::cout << "\n删除操作:" << std::endl;
rootFolder->remove("image.png");
rootFolder->display();
return 0;
}
代码解析
-
FileSystemComponent类 :抽象组件,定义了文件和文件夹的共同接口(如
display()
),并提供了默认不支持的子组件操作方法(add()
、remove()
等)。 -
File类 :叶子节点,代表单个文件,实现了
display()
方法展示文件信息,不支持子组件操作(使用父类的默认实现会抛出异常)。 -
Folder类 :复合节点,代表文件夹,维护一个子组件列表(可以是文件或其他文件夹),实现了所有子组件操作方法,并在
display()
中递归显示所有子组件。 -
客户端处理:通过抽象组件接口统一处理文件和文件夹,无需区分是单个对象还是组合对象,体现了"单一职责"和"开闭"原则。
组合模式的优缺点
优点:
- 统一处理单个对象和组合对象,简化客户端代码
- 易于扩展新的组件类型(叶子或复合节点),符合开放-封闭原则
- 可以灵活地构建复杂的树形结构
- 便于对整个树形结构进行操作(如递归遍历)
缺点:
- 可能使设计过于泛化,增加理解难度
- 对于需要限制子节点类型的场景,实现起来较复杂
- 递归操作可能影响性能(对于极深的树形结构)
适用场景
- 当需要表示对象的部分-整体层次结构(树形结构)时
- 当希望客户端以统一方式处理单个对象和组合对象时
- 当需要动态地添加/移除子组件时
常见应用:
- 文件系统(文件和文件夹)
- GUI组件树(按钮、面板、窗口等)
- 组织结构(部门和员工)
- XML/JSON等树形数据结构的解析
组合模式与装饰器模式的区别:组合模式关注对象的部分-整体关系,而装饰器模式关注动态为对象添加功能。两者都使用了递归组合,但目的不同。