设计模式 | 组合模式

发布于:2025-06-30 ⋅ 阅读:(21) ⋅ 点赞:(0)

组合模式(Composite Pattern) 是结构型设计模式中的层次管理大师,它允许你将对象组合成树形结构来表示"部分-整体"的层次关系。本文将深入探索组合模式的核心思想、实现技巧以及在C++中的高效实践,解决复杂树形结构的统一管理问题。

为什么需要组合模式?

在软件开发中,我们经常需要处理树形结构的数据:

  • 文件系统中的目录与文件

  • GUI中的容器与控件

  • 组织架构中的部门与员工

  • 产品分类中的类别与产品

传统处理方式的问题:

  • 不一致的接口:叶子节点和容器节点接口不同

  • 复杂的递归逻辑:处理树形结构需要大量递归代码

  • 代码重复:相似逻辑分散在不同类型节点中

  • 扩展困难:新增节点类型需要修改现有代码

组合模式通过统一叶子与容器的接口解决了这些问题,提供了优雅的树形结构管理方案。

组合模式的核心概念

模式结构解析

          [组件接口]
            ▲
            |
    -----------------
    |               |
[叶子节点]       [容器节点] → [子组件]

关键角色定义

  1. 组件(Component)

    • 定义所有对象的通用接口

    • 声明管理子组件的接口(可选)

  2. 叶子(Leaf)

    • 表示树中的叶子节点(没有子节点)

    • 实现组件接口

  3. 容器(Composite)

    • 表示树中的分支节点(有子节点)

    • 实现组件接口

    • 存储并管理子组件

C++实现:文件系统模拟

让我们实现一个文件系统模拟,展示组合模式的实际应用:

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

// ================= 组件接口:文件系统项 =================
class FileSystemComponent {
public:
    virtual ~FileSystemComponent() = default;
    
    virtual std::string getName() const = 0;
    virtual int getSize() const = 0;
    virtual void display(int depth = 0) const = 0;
    
    // 容器管理方法(叶子节点不实现)
    virtual void add(std::shared_ptr<FileSystemComponent> component) {
        throw std::runtime_error("不支持的操作: add");
    }
    
    virtual void remove(std::shared_ptr<FileSystemComponent> component) {
        throw std::runtime_error("不支持的操作: remove");
    }
    
    virtual std::shared_ptr<FileSystemComponent> getChild(int index) const {
        throw std::runtime_error("不支持的操作: getChild");
    }
};

// ================= 叶子节点:文件 =================
class File : public FileSystemComponent {
public:
    File(std::string name, int size) 
        : name_(std::move(name)), size_(size) {}
    
    std::string getName() const override {
        return name_;
    }
    
    int getSize() const override {
        return size_;
    }
    
    void display(int depth = 0) const override {
        std::cout << std::string(depth * 2, ' ') 
                  << "- " << name_ 
                  << " (" << size_ << " bytes)\n";
    }

private:
    std::string name_;
    int size_;
};

// ================= 容器节点:目录 =================
class Directory : public FileSystemComponent {
public:
    explicit Directory(std::string name) 
        : name_(std::move(name)) {}
    
    std::string getName() const override {
        return name_;
    }
    
    int getSize() const override {
        int totalSize = 0;
        for (const auto& item : children_) {
            totalSize += item->getSize();
        }
        return totalSize;
    }
    
    void add(std::shared_ptr<FileSystemComponent> component) override {
        children_.push_back(component);
    }
    
    void remove(std::shared_ptr<FileSystemComponent> component) override {
        auto it = std::find(children_.begin(), children_.end(), component);
        if (it != children_.end()) {
            children_.erase(it);
        }
    }
    
    std::shared_ptr<FileSystemComponent> getChild(int index) const override {
        if (index < 0 || index >= children_.size()) {
            return nullptr;
        }
        return children_[index];
    }
    
    void display(int depth = 0) const override {
        std::cout << std::string(depth * 2, ' ') 
                  << "+ " << name_ 
                  << " [目录, 大小: " << getSize() << " bytes]\n";
        
        for (const auto& child : children_) {
            child->display(depth + 1);
        }
    }

private:
    std::string name_;
    std::vector<std::shared_ptr<FileSystemComponent>> children_;
};

// ================= 客户端代码 =================
int main() {
    // 创建文件系统结构
    auto root = std::make_shared<Directory>("根目录");
    
    // 添加文件到根目录
    root->add(std::make_shared<File>("系统日志.txt", 1024));
    root->add(std::make_shared<File>("README.md", 512));
    
    // 创建子目录
    auto documents = std::make_shared<Directory>("文档");
    auto images = std::make_shared<Directory>("图片");
    auto music = std::make_shared<Directory>("音乐");
    
    // 添加文档
    documents->add(std::make_shared<File>("报告.docx", 2048));
    documents->add(std::make_shared<File>("预算表.xlsx", 4096));
    
    // 添加图片
    images->add(std::make_shared<File>("头像.jpg", 3072));
    images->add(std::make_shared<File>("风景.png", 8192));
    
    // 添加音乐
    auto rock = std::make_shared<Directory>("摇滚");
    music->add(rock);
    rock->add(std::make_shared<File>("摇滚经典1.mp3", 5120));
    rock->add(std::make_shared<File>("摇滚经典2.mp3", 6144));
    
    auto jazz = std::make_shared<Directory>("爵士");
    music->add(jazz);
    jazz->add(std::make_shared<File>("爵士乐1.mp3", 2560));
    jazz->add(std::make_shared<File>("爵士乐2.mp3", 3584));
    
    // 将子目录添加到根目录
    root->add(documents);
    root->add(images);
    root->add(music);
    
    // 显示整个文件系统
    std::cout << "===== 文件系统结构 =====\n";
    root->display();
    
    // 计算总大小
    std::cout << "\n===== 统计信息 =====\n";
    std::cout << "根目录总大小: " << root->getSize() << " bytes\n";
    std::cout << "音乐目录大小: " << music->getSize() << " bytes\n";
    std::cout << "摇滚目录大小: " << rock->getSize() << " bytes\n";
    
    // 查找特定文件
    std::cout << "\n===== 查找文件 =====\n";
    auto findFile = [](const std::shared_ptr<FileSystemComponent>& root, 
                       const std::string& name) -> std::shared_ptr<FileSystemComponent> {
        if (root->getName() == name) {
            return root;
        }
        
        try {
            for (int i = 0; ; ++i) {
                auto child = root->getChild(i);
                if (!child) break;
                
                if (auto found = findFile(child, name)) {
                    return found;
                }
            }
        } catch (const std::runtime_error&) {
            // 叶子节点没有子节点,忽略异常
        }
        
        return nullptr;
    };
    
    if (auto file = findFile(root, "预算表.xlsx")) {
        std::cout << "找到文件: " << file->getName() 
                  << ", 大小: " << file->getSize() << " bytes\n";
    }
    
    return 0;
}

组合模式的四大优势

1. 统一处理简单和复杂元素

// 统一接口处理文件和目录
void processItem(FileSystemComponent* item) {
    std::cout << "名称: " << item->getName() 
              << ", 大小: " << item->getSize() << "\n";
    item->display();
    
    // 无论item是文件还是目录,都能正常工作
}

2. 简化客户端代码

// 客户端不需要区分文件和目录
void printStructure(FileSystemComponent* component) {
    component->display(); // 递归逻辑封装在组件内部
}

3. 轻松添加新元素类型

// 新增符号链接类型
class SymLink : public FileSystemComponent {
    // 实现组件接口
};

// 客户端代码无需修改即可使用
root->add(std::make_shared<SymLink>("快捷方式", target));

4. 递归操作简化

// 递归计算大小封装在组件中
int totalSize = root->getSize(); // 递归逻辑对客户端透明

组合模式的高级应用

1. 访问者模式组合

class FileSystemVisitor {
public:
    virtual void visitFile(File* file) = 0;
    virtual void visitDirectory(Directory* dir) = 0;
};

class FileSystemComponent {
public:
    virtual void accept(FileSystemVisitor& visitor) = 0;
};

class File : public FileSystemComponent {
    void accept(FileSystemVisitor& visitor) override {
        visitor.visitFile(this);
    }
};

class Directory : public FileSystemComponent {
    void accept(FileSystemVisitor& visitor) override {
        visitor.visitDirectory(this);
        for (auto& child : children_) {
            child->accept(visitor);
        }
    }
};

// 实现具体访问者
class SizeCalculator : public FileSystemVisitor {
    int totalSize = 0;
    void visitFile(File* file) override {
        totalSize += file->getSize();
    }
    void visitDirectory(Directory* dir) override {
        // 目录本身不增加大小
    }
};

2. 透明与安全组合模式

透明模式

// 所有组件都有add/remove方法(叶子抛出异常)
class Component {
public:
    virtual void add(Component* c) = 0; // 叶子节点也实现
};

安全模式

// 只有容器有add/remove方法
class Component {
    // 没有add/remove方法
};

class Composite : public Component {
    void add(Component* c) override; // 容器实现
};

3. 组合模式与享元模式结合

// 共享叶子节点
class FileFactory {
    static std::shared_ptr<File> getFile(const std::string& name, int size) {
        static std::map<std::pair<std::string, int>, std::shared_ptr<File>> files;
        auto key = std::make_pair(name, size);
        if (!files[key]) {
            files[key] = std::make_shared<File>(name, size);
        }
        return files[key];
    }
};

// 使用共享文件
dir->add(FileFactory::getFile("logo.png", 1024));

组合模式的应用场景

1. GUI框架设计

// 组件基类
class Widget {
public:
    virtual void render() const = 0;
    virtual void add(std::shared_ptr<Widget> child) {
        throw std::runtime_error("不支持添加子组件");
    }
};

// 叶子组件:按钮
class Button : public Widget {
    void render() const override {
        std::cout << "渲染按钮: " << text_ << "\n";
    }
};

// 容器组件:面板
class Panel : public Widget {
    void render() const override {
        std::cout << "开始渲染面板\n";
        for (auto& child : children_) {
            child->render();
        }
        std::cout << "结束渲染面板\n";
    }
    
    void add(std::shared_ptr<Widget> child) override {
        children_.push_back(child);
    }
};

// 使用
auto panel = std::make_shared<Panel>();
panel->add(std::make_shared<Button>("确定"));
panel->add(std::make_shared<Button>("取消"));
panel->render();

2. 组织架构管理

class OrganizationComponent {
public:
    virtual std::string getName() const = 0;
    virtual double getCost() const = 0; // 部门成本或员工薪资
};

// 员工(叶子)
class Employee : public OrganizationComponent {
    double getCost() const override { return salary_; }
};

// 部门(容器)
class Department : public OrganizationComponent {
    double getCost() const override {
        double total = 0;
        for (auto& member : members_) {
            total += member->getCost();
        }
        return total + operationalCost_;
    }
};

3. 游戏场景图

class SceneNode {
public:
    virtual void update(float deltaTime) = 0;
    virtual void render() const = 0;
};

// 游戏对象(叶子)
class GameObject : public SceneNode {
    void update(float deltaTime) override {
        // 更新位置、状态等
    }
    
    void render() const override {
        // 渲染对象
    }
};

// 场景组(容器)
class SceneGroup : public SceneNode {
    void update(float deltaTime) override {
        for (auto& child : children_) {
            child->update(deltaTime);
        }
    }
    
    void render() const override {
        for (auto& child : children_) {
            child->render();
        }
    }
};

组合模式的五大优势

  1. 统一接口处理简单和复杂元素

    // 统一操作接口
    void backup(FileSystemComponent* item) {
        BackupSystem::save(item->getName(), item->getContent());
        if (auto dir = dynamic_cast<Directory*>(item)) {
            for (int i = 0; i < dir->childCount(); i++) {
                backup(dir->getChild(i));
            }
        }
    }
  2. 简化客户端代码

    // 客户端无需关心具体类型
    void printItem(FileSystemComponent* item) {
        item->display(); // 文件或目录都能处理
    }
  3. 易于扩展新组件类型

    // 新增压缩文件类型
    class CompressedFile : public FileSystemComponent {
        // 实现组件接口
    };
    
    // 现有代码无需修改
    root->add(std::make_shared<CompressedFile>("archive.zip"));
  4. 递归操作简化

    // 递归计算大小
    int totalSize = root->getSize(); // 单行代码完成递归计算
  5. 层次结构管理灵活

    // 动态重组结构
    dir->remove(file);
    anotherDir->add(file);

组合模式的最佳实践

1. 合理设计组件接口

class Component {
public:
    // 通用操作
    virtual void operation() = 0;
    
    // 子组件管理(为叶子提供默认空实现)
    virtual void add(Component* c) {}
    virtual void remove(Component* c) {}
    virtual Component* getChild(int) { return nullptr; }
    
    // 可选操作
    virtual bool isComposite() const { return false; }
};

2. 使用智能指针管理内存

class Directory : public FileSystemComponent {
private:
    std::vector<std::shared_ptr<FileSystemComponent>> children_;
};

// 使用
auto root = std::make_shared<Directory>("root");
root->add(std::make_shared<File>("file.txt", 100));

3. 实现空对象模式

class NullComponent : public FileSystemComponent {
    std::string getName() const override { return "Null"; }
    int getSize() const override { return 0; }
    void display(int) const override {}
};

// 使用
auto child = dir->getChild(10);
if (dynamic_cast<NullComponent*>(child.get())) {
    // 处理空对象情况
}

4. 优化容器性能

class OptimizedDirectory : public Directory {
public:
    int getSize() const override {
        if (sizeCacheDirty_) {
            sizeCache_ = Directory::getSize();
            sizeCacheDirty_ = false;
        }
        return sizeCache_;
    }
    
    void add(std::shared_ptr<FileSystemComponent> c) override {
        Directory::add(c);
        sizeCacheDirty_ = true;
    }
    
    // 类似实现remove等方法
};

组合模式与其他模式的关系

模式 关系 区别
装饰器模式 都使用递归组合 装饰器添加职责,组合构建树结构
访问者模式 常用组合遍历 访问者分离操作与结构
迭代器模式 组合需要迭代器 迭代器遍历组合结构
享元模式 可共享叶子节点 享元节省内存,组合管理结构

组合使用示例

// 组合模式 + 访问者模式
class FileSystemComponent {
public:
    virtual void accept(FileSystemVisitor& visitor) = 0;
};

class FileSystemVisitor {
public:
    virtual void visitFile(File* file) = 0;
    virtual void visitDirectory(Directory* dir) = 0;
};

class File : public FileSystemComponent {
    void accept(FileSystemVisitor& visitor) override {
        visitor.visitFile(this);
    }
};

class Directory : public FileSystemComponent {
    void accept(FileSystemVisitor& visitor) override {
        visitor.visitDirectory(this);
        for (auto& child : children_) {
            child->accept(visitor);
        }
    }
};

应用案例

1. XML/HTML文档处理

class XMLNode {
public:
    virtual void render(int indent = 0) const = 0;
};

class XMLElement : public XMLNode {
    void render(int indent = 0) const override {
        std::cout << std::string(indent, ' ') << "<" << tagName_ << ">\n";
        for (auto& child : children_) {
            child->render(indent + 2);
        }
        std::cout << std::string(indent, ' ') << "</" << tagName_ << ">\n";
    }
};

class XMLTextNode : public XMLNode {
    void render(int indent = 0) const override {
        std::cout << std::string(indent, ' ') << text_ << "\n";
    }
};

// 构建文档
auto root = std::make_shared<XMLElement>("html");
auto body = std::make_shared<XMLElement>("body");
root->add(body);
body->add(std::make_shared<XMLElement>("h1")->addText("标题"));
body->add(std::make_shared<XMLElement>("p")->addText("段落内容"));
root->render();

2. 数学表达式处理

class Expression {
public:
    virtual double evaluate() const = 0;
};

class Number : public Expression {
    double evaluate() const override { return value_; }
};

class BinaryOperation : public Expression {
    double evaluate() const override {
        double left = left_->evaluate();
        double right = right_->evaluate();
        
        switch (op_) {
            case '+': return left + right;
            case '-': return left - right;
            case '*': return left * right;
            case '/': return left / right;
            default: throw std::runtime_error("未知操作符");
        }
    }
};

// 构建表达式树: (2 + 3) * 4
auto expr = std::make_shared<BinaryOperation>(
    '*',
    std::make_shared<BinaryOperation>('+', 
        std::make_shared<Number>(2), 
        std::make_shared<Number>(3)),
    std::make_shared<Number>(4)
);

std::cout << "结果: " << expr->evaluate() << "\n"; // 输出 20

3. 自动化测试框架

class TestComponent {
public:
    virtual void run() = 0;
};

class TestCase : public TestComponent {
    void run() override {
        // 执行单个测试用例
    }
};

class TestSuite : public TestComponent {
    void run() override {
        // 运行所有测试用例
        for (auto& test : tests_) {
            test->run();
        }
    }
};

// 构建测试套件
auto regressionSuite = std::make_shared<TestSuite>();
regressionSuite->add(std::make_shared<TestCase>("登录测试"));
regressionSuite->add(std::make_shared<TestCase>("支付测试"));

auto smokeSuite = std::make_shared<TestSuite>();
smokeSuite->add(std::make_shared<TestCase>("主页加载测试"));
smokeSuite->add(regressionSuite);

// 运行所有测试
smokeSuite->run();

组合模式的挑战与解决方案

挑战 解决方案
过度通用化接口 为叶子节点提供默认空实现
性能问题 添加缓存机制优化计算
类型检查问题 使用访问者模式替代类型检查
循环引用 使用弱引用或禁止父引用

循环引用解决方案

class Directory : public FileSystemComponent {
public:
    void setParent(std::weak_ptr<Directory> parent) {
        parent_ = parent;
    }
    
    std::shared_ptr<Directory> getParent() const {
        return parent_.lock();
    }

private:
    std::weak_ptr<Directory> parent_; // 使用弱引用避免循环
};

总结

组合模式通过统一接口管理树形结构,提供了强大的层次管理能力:

  1. 统一接口:一致处理简单元素和复杂结构

  2. 递归简化:复杂递归操作封装在组件内部

  3. 灵活扩展:轻松添加新元素类型

  4. 结构清晰:自然表示部分-整体层次关系

  5. 代码复用:共享树结构操作逻辑

使用时机

  • 需要表示对象的整体-部分层次结构

  • 希望客户端忽略组合与单个对象的不同

  • 需要统一处理简单元素和复杂结构

  • 系统需要灵活地添加新组件类型

"组合模式不是简单地存储对象,而是在树形结构中统一管理简单与复杂的艺术。它是面向对象设计中处理层次结构的精妙解决方案。" - 设计模式实践者


网站公告

今日签到

点亮在社区的每一天
去签到