【C++】组合模式

发布于:2025-07-01 ⋅ 阅读:(20) ⋅ 点赞:(0)

组合模式(Composite Pattern)是一种【结构型】设计模式,它允许你将对象组合成树形结构以表示 “部分 - 整体” 的层次关系。这种模式使得客户端可以统一处理单个对象和对象组合,无需区分它们的具体类型。

一、模式核心概念与结构

组合模式包含三个核心角色:

  1. 组件(Component):定义组合中所有对象的通用接口,声明管理子组件的方法。
  2. 叶节点(Leaf):表示组合中的叶节点对象,没有子节点,实现组件接口。
  3. 组合节点(Composite):表示组合中的分支节点,包含子组件,实现组件接口并管理子组件。

二、C++ 实现示例:文件系统

以下是一个经典的组合模式示例,演示如何用组合模式表示文件系统:

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

// 组件:文件系统元素
class FileSystemElement {
public:
    virtual ~FileSystemElement() = default;
    virtual void print(int depth = 0) const = 0;
    virtual size_t getSize() const = 0;
    virtual void add(std::shared_ptr<FileSystemElement> element) {}
    virtual void remove(std::shared_ptr<FileSystemElement> element) {}
};

// 叶节点:文件
class File : public FileSystemElement {
private:
    std::string name;
    size_t size;

public:
    File(const std::string& n, size_t s) : name(n), size(s) {}
    
    void print(int depth) const override {
        std::cout << std::string(depth * 2, ' ') << "- " << name 
                  << " (file, " << size << " bytes)" << std::endl;
    }
    
    size_t getSize() const override {
        return size;
    }
};

// 组合节点:目录
class Directory : public FileSystemElement {
private:
    std::string name;
    std::vector<std::shared_ptr<FileSystemElement>> children;

public:
    Directory(const std::string& n) : name(n) {}
    
    void print(int depth) const override {
        std::cout << std::string(depth * 2, ' ') << "+ " << name 
                  << " (directory, " << getSize() << " bytes)" << std::endl;
        for (const auto& child : children) {
            child->print(depth + 1);
        }
    }
    
    size_t getSize() const override {
        size_t total = 0;
        for (const auto& child : children) {
            total += child->getSize();
        }
        return total;
    }
    
    void add(std::shared_ptr<FileSystemElement> element) override {
        children.push_back(element);
    }
    
    void remove(std::shared_ptr<FileSystemElement> element) override {
        for (auto it = children.begin(); it != children.end(); ++it) {
            if (*it == element) {
                children.erase(it);
                break;
            }
        }
    }
};

// 客户端代码
int main() {
    // 创建目录结构
    auto root = std::make_shared<Directory>("/");
    auto home = std::make_shared<Directory>("home");
    auto user = std::make_shared<Directory>("user");
    auto docs = std::make_shared<Directory>("documents");
    
    // 添加文件
    docs->add(std::make_shared<File>("report.txt", 1024));
    docs->add(std::make_shared<File>("presentation.pdf", 5120));
    user->add(docs);
    user->add(std::make_shared<File>("profile.jpg", 2048));
    home->add(user);
    root->add(home);
    root->add(std::make_shared<File>("readme.txt", 512));
    
    // 打印文件系统结构
    root->print();
    
    // 计算总大小
    std::cout << "\nTotal size: " << root->getSize() << " bytes" << std::endl;
    
    return 0;
}

三、组合模式的关键特性

  1. 统一接口
    • 组件接口定义了叶节点和组合节点的共同行为(如print()getSize())。
    • 客户端可以一致地处理单个对象和对象组合。
  2. 递归结构
    • 组合节点可以包含其他组合节点或叶节点,形成树形结构。
    • 操作可以递归地应用于整个树结构。
  3. 透明性 vs 安全性
    • 透明性:在组件接口中声明所有管理子节点的方法(如add()remove()),使叶节点和组合节点具有相同接口,但可能导致叶节点运行时错误。
    • 安全性:仅在组合节点中声明管理子节点的方法,叶节点不包含这些方法,但客户端需区分叶节点和组合节点。

四、应用场景

  1. 树形结构表示
    • 文件系统、XML/JSON 解析树。
    • 组织结构图、菜单系统。
  2. 统一处理对象
    • 图形编辑器中的形状组合(如 Group、Layer)。
    • 游戏中的场景图(Scene Graph)。
  3. 递归操作
    • 数学表达式计算(如加减乘除组合)。
    • 权限管理中的角色和权限组。

五、组合模式与其他设计模式的关系

  1. 迭代器模式
    • 组合模式常与迭代器模式结合,用于遍历树形结构。
    • 例如,使用迭代器遍历文件系统中的所有文件。
  2. 访问者模式
    • 组合模式可以配合访问者模式,将算法与对象结构分离。
    • 例如,通过访问者模式实现文件系统的大小统计、搜索等操作。
  3. 享元模式
    • 组合模式的叶节点可以是享元对象,共享内部状态以节省内存。
    • 例如,文件系统中的相同文件可以共享同一个对象实例。

六、C++ 标准库中的组合模式应用

  1. STL 容器
    • std::vectorstd::list等容器可以存储不同类型的元素,形成树形结构。
    • 例如,std::vector<std::shared_ptr<Component>>可以存储组合节点和叶节点。
  2. 智能指针
    • std::shared_ptrstd::unique_ptr可用于管理组合结构中的对象生命周期。
    • 例如,在文件系统示例中使用std::shared_ptr避免内存泄漏。
  3. 流类库
    • std::iostream层次结构中,std::iostream是抽象组件,std::ifstreamstd::ofstream是叶节点,std::stringstream可视为组合节点。

七、优缺点分析

优点:

  • 简化客户端代码:客户端无需区分处理叶节点和组合节点。
  • 灵活扩展:可以轻松添加新的叶节点或组合节点。
  • 树形结构清晰:明确表示 “部分 - 整体” 的层次关系。

缺点:

  • 限制类型安全:透明实现可能导致运行时错误(如对叶节点调用add())。
  • 设计复杂度:在某些情况下,过度使用组合模式可能使设计变得复杂。
  • 性能问题:递归操作可能导致性能开销,尤其是大型树结构。

八、实战案例:图形编辑器

以下是一个图形编辑器的组合模式实现:

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

// 组件:图形元素
class Shape {
public:
    virtual ~Shape() = default;
    virtual void draw() const = 0;
    virtual double area() const = 0;
    virtual void add(std::shared_ptr<Shape> shape) {}
    virtual void remove(std::shared_ptr<Shape> shape) {}
};

// 叶节点:圆形
class Circle : public Shape {
private:
    double radius;
    std::string color;

public:
    Circle(double r, const std::string& c) : radius(r), color(c) {}
    
    void draw() const override {
        std::cout << "Drawing Circle with radius " << radius 
                  << " and color " << color << std::endl;
    }
    
    double area() const override {
        return M_PI * radius * radius;
    }
};

// 叶节点:矩形
class Rectangle : public Shape {
private:
    double width, height;
    std::string color;

public:
    Rectangle(double w, double h, const std::string& c) : width(w), height(h), color(c) {}
    
    void draw() const override {
        std::cout << "Drawing Rectangle with width " << width 
                  << ", height " << height << " and color " << color << std::endl;
    }
    
    double area() const override {
        return width * height;
    }
};

// 组合节点:图形组
class Group : public Shape {
private:
    std::string name;
    std::vector<std::shared_ptr<Shape>> shapes;

public:
    Group(const std::string& n) : name(n) {}
    
    void draw() const override {
        std::cout << "Group " << name << " contains:" << std::endl;
        for (const auto& shape : shapes) {
            shape->draw();
        }
    }
    
    double area() const override {
        double total = 0;
        for (const auto& shape : shapes) {
            total += shape->area();
        }
        return total;
    }
    
    void add(std::shared_ptr<Shape> shape) override {
        shapes.push_back(shape);
    }
    
    void remove(std::shared_ptr<Shape> shape) override {
        for (auto it = shapes.begin(); it != shapes.end(); ++it) {
            if (*it == shape) {
                shapes.erase(it);
                break;
            }
        }
    }
};

// 客户端代码
int main() {
    // 创建图形元素
    auto circle = std::make_shared<Circle>(5.0, "red");
    auto rectangle = std::make_shared<Rectangle>(4.0, 6.0, "blue");
    
    // 创建图形组
    auto group1 = std::make_shared<Group>("Group 1");
    group1->add(circle);
    group1->add(rectangle);
    
    // 创建另一个图形组并添加子组
    auto group2 = std::make_shared<Group>("Group 2");
    auto anotherCircle = std::make_shared<Circle>(3.0, "green");
    group2->add(anotherCircle);
    group2->add(group1);  // 添加子组
    
    // 绘制所有图形
    group2->draw();
    
    // 计算总面积
    std::cout << "\nTotal area: " << group2->area() << std::endl;
    
    return 0;
}

九、实现注意事项

  1. 内存管理
    • 使用智能指针(如std::shared_ptr)管理组合结构中的对象生命周期。
    • 避免循环引用导致内存泄漏(可使用std::weak_ptr)。
  2. 接口设计
    • 根据需要选择透明性(在基类中声明所有方法)或安全性(仅在组合类中声明特定方法)。
  3. 递归深度
    • 对于大型树结构,递归操作可能导致栈溢出,考虑使用迭代或尾递归优化。
  4. 线程安全
    • 在多线程环境中,修改组合结构(如add()remove())需考虑同步问题。

组合模式是 C++ 中处理树形结构的重要工具,通过统一接口和递归组合,使客户端可以一致地处理单个对象和对象组合,从而简化了代码设计并提高了系统的可扩展性。


如果这篇文章对你有所帮助,渴望获得你的一个点赞!

在这里插入图片描述


网站公告

今日签到

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