C++ Visitor 模式:在不修改类结构的情况下添加新操作

哈喽,各位好!今天咱们来聊聊C++中的Visitor模式,一个能让你在不改动现有类结构的前提下,给它们“穿新衣戴新帽”的神奇设计模式。说白了,就是给你的类增加新功能,但又不想动它们的老代码。

故事的开始:一个简单的图形系统

想象一下,你正在开发一个图形系统,里面有圆形(Circle)、矩形(Rectangle)和三角形(Triangle)三种基本图形。每个图形都有自己的绘制(draw)方法,用来在屏幕上显示自己。

#include <iostream>
#include <vector>

class Shape {
public:
    virtual void draw() = 0;
    virtual ~Shape() {} // 记得加虚析构函数,防止内存泄漏
};

class Circle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing Circlen";
    }
};

class Rectangle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing Rectanglen";
    }
};

class Triangle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing Trianglen";
    }
};

int main() {
    std::vector<Shape*> shapes;
    shapes.push_back(new Circle());
    shapes.push_back(new Rectangle());
    shapes.push_back(new Triangle());

    for (Shape* shape : shapes) {
        shape->draw();
    }

    // 别忘了释放内存,好习惯!
    for (Shape* shape : shapes) {
        delete shape;
    }
    shapes.clear();

    return 0;
}

现在,需求来了!老板说:“我们要能计算这些图形的面积,还要能把它们导出成SVG格式!”

直接修改?No Way!

最直接的想法就是在每个图形类里加上 calculateArea()exportToSVG() 方法。

// 如果我们直接修改类...
class Circle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing Circlen";
    }
    double calculateArea() {
        // 计算圆形面积
        return 3.14159; // 简化
    }
    void exportToSVG() {
        std::cout << "<circle ... />n"; // 简化
    }
};

// Rectangle 和 Triangle 类似...

这样做的问题是:

  • 违反开闭原则: 每次新增操作都要修改已有的类,这违反了“对扩展开放,对修改关闭”的原则。万一这些类在很多地方被使用,改动起来风险很大。
  • 代码膨胀: 每个图形类都要重复实现类似的功能,代码冗余。
  • 职责不单一: 图形类既负责图形本身的属性,又负责计算面积和导出格式,职责不单一,不利于维护。

Visitor 模式闪亮登场

Visitor模式就像一个“访问者”,它可以“拜访”不同的图形,并对它们执行特定的操作,而无需修改图形类本身。

核心角色:

  • Visitor (访问者): 定义了访问每个 Element (也就是我们的图形) 的接口。每个具体的操作都对应一个 visit 方法。
  • ConcreteVisitor (具体访问者): 实现了 Visitor 接口,对每个 Element 执行具体的操作。比如,AreaCalculator 负责计算面积,SVGExporter 负责导出SVG。
  • Element (元素): 定义了 accept() 方法,允许 Visitor 访问自己。
  • ConcreteElement (具体元素): 实现了 Element 接口,在 accept() 方法中调用 Visitor 相应的 visit 方法,并将自己作为参数传递给 Visitor
  • ObjectStructure (对象结构): 可选角色,负责管理 Element 集合,并让 Visitor 依次访问每个 Element

代码实现:

  1. Shape 类(Element):
class Shape {
public:
    virtual void accept(class Visitor* visitor) = 0; // 关键的 accept 方法
    virtual void draw() = 0;
    virtual ~Shape() {}
};
  1. Circle, Rectangle, Triangle 类(ConcreteElement):
class Circle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing Circlen";
    }
    void accept(Visitor* visitor) override; // forward declaration needed

};

class Rectangle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing Rectanglen";
    }
    void accept(Visitor* visitor) override;
};

class Triangle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing Trianglen";
    }
    void accept(Visitor* visitor) override;
};
  1. Visitor 接口:
class Visitor {
public:
    virtual void visit(Circle* circle) = 0;
    virtual void visit(Rectangle* rectangle) = 0;
    virtual void visit(Triangle* triangle) = 0;
    virtual ~Visitor() {}
};
  1. AreaCalculator 类(ConcreteVisitor):
class AreaCalculator : public Visitor {
public:
    double circleArea;
    double rectangleArea;
    double triangleArea;

    AreaCalculator() : circleArea(0.0), rectangleArea(0.0), triangleArea(0.0) {}

    void visit(Circle* circle) override {
        circleArea = 3.14 * 5 * 5; // 假设半径为5
    }
    void visit(Rectangle* rectangle) override {
        rectangleArea = 10 * 5; // 假设长为10,宽为5
    }
    void visit(Triangle* triangle) override {
        triangleArea = 0.5 * 8 * 6; // 假设底为8,高为6
    }
};
  1. SVGExporter 类(ConcreteVisitor):
class SVGExporter : public Visitor {
public:
    std::string circleSVG;
    std::string rectangleSVG;
    std::string triangleSVG;

    void visit(Circle* circle) override {
        circleSVG = "<circle cx='50' cy='50' r='5' fill='red' />"; // 简化
    }
    void visit(Rectangle* rectangle) override {
        rectangleSVG = "<rect x='10' y='10' width='10' height='5' fill='blue' />"; // 简化
    }
    void visit(Triangle* triangle) override {
        triangleSVG = "<polygon points='0,0 10,0 5,8' fill='green' />"; // 简化
    }
};
  1. 实现 accept 方法:
void Circle::accept(Visitor* visitor) {
    visitor->visit(this);
}

void Rectangle::accept(Visitor* visitor) {
    visitor->visit(this);
}

void Triangle::accept(Visitor* visitor) {
    visitor->visit(this);
}
  1. 使用 Visitor:
int main() {
    std::vector<Shape*> shapes;
    shapes.push_back(new Circle());
    shapes.push_back(new Rectangle());
    shapes.push_back(new Triangle());

    // 计算面积
    AreaCalculator areaCalculator;
    for (Shape* shape : shapes) {
        shape->accept(&areaCalculator);
    }
    std::cout << "Circle Area: " << areaCalculator.circleArea << std::endl;
    std::cout << "Rectangle Area: " << areaCalculator.rectangleArea << std::endl;
    std::cout << "Triangle Area: " << areaCalculator.triangleArea << std::endl;

    // 导出 SVG
    SVGExporter svgExporter;
    for (Shape* shape : shapes) {
        shape->accept(&svgExporter);
    }
    std::cout << "Circle SVG: " << svgExporter.circleSVG << std::endl;
    std::cout << "Rectangle SVG: " << svgExporter.rectangleSVG << std::endl;
    std::cout << "Triangle SVG: " << svgExporter.triangleSVG << std::endl;

    // 释放内存
    for (Shape* shape : shapes) {
        delete shape;
    }
    shapes.clear();

    return 0;
}

ObjectStructure 的应用 (可选)

如果你的图形集合非常复杂,或者你需要对图形进行统一管理,可以使用 ObjectStructure

#include <vector>
#include <algorithm>

class ObjectStructure {
private:
    std::vector<Shape*> shapes;

public:
    void add(Shape* shape) {
        shapes.push_back(shape);
    }

    void remove(Shape* shape) {
        // 使用 erase-remove idiom 安全移除元素
        shapes.erase(std::remove(shapes.begin(), shapes.end(), shape), shapes.end());
    }

    void accept(Visitor* visitor) {
        for (Shape* shape : shapes) {
            shape->accept(visitor);
        }
    }

    ~ObjectStructure(){
        for(auto shape: shapes){
            delete shape;
        }
        shapes.clear();
    }
};

int main() {
    ObjectStructure shapes;
    shapes.add(new Circle());
    shapes.add(new Rectangle());
    shapes.add(new Triangle());

    // 计算面积
    AreaCalculator areaCalculator;
    shapes.accept(&areaCalculator);

    std::cout << "Circle Area: " << areaCalculator.circleArea << std::endl;
    std::cout << "Rectangle Area: " << areaCalculator.rectangleArea << std::endl;
    std::cout << "Triangle Area: " << areaCalculator.triangleArea << std::endl;

    return 0;
}

Visitor 模式的优点:

  • 开闭原则: 可以方便地添加新的操作,而无需修改现有的类结构。
  • 职责分离: 将操作与对象结构分离,使代码更清晰、更易于维护。
  • 灵活性: 可以根据需要组合不同的 Visitor,执行不同的操作。

Visitor 模式的缺点:

  • 复杂性增加: 引入了额外的类和接口,增加了代码的复杂性。
  • 类型依赖: Visitor 需要知道所有 ConcreteElement 的类型,如果新增 Element,需要修改 Visitor 接口和所有 ConcreteVisitor。这被称为“Visitor 模式的双重分派问题”。
  • 破坏封装: 如果Visitor需要访问Element的私有成员,可能会需要将这些成员设置为public或者提供friend访问,从而破坏了封装性。

何时使用 Visitor 模式:

  • 需要对一个对象结构(一组对象)执行多种不同的操作,而这些操作之间没有必然联系。
  • 希望避免在对象结构中添加大量重复的代码。
  • 对象结构稳定,但操作可能会频繁变化。

Visitor 模式的应用场景:

  • 编译器: 语法树的遍历和分析。
  • 文档处理: 对文档对象模型(DOM)进行各种操作,如格式化、验证等。
  • 图形编辑器: 对图形对象进行各种操作,如绘制、选择、移动等。

与其它模式的比较:

模式 优点 缺点 适用场景
Visitor 开闭原则,职责分离,灵活性 复杂性增加,类型依赖,可能破坏封装 需要对对象结构执行多种不同操作,对象结构稳定,操作可能频繁变化
Strategy 定义算法簇,可以动态切换算法。 客户端需要知道所有策略类,并选择合适的策略。 需要动态选择算法,算法之间可以互换。
Command 将请求封装成对象,可以实现请求的排队、撤销、重做等功能。 可能产生大量的具体命令类。 需要将请求封装成对象,并支持撤销、重做等功能。
Template Method 定义算法的骨架,将一些步骤延迟到子类实现。 子类必须遵守父类的算法骨架。 需要定义算法的骨架,并将一些步骤延迟到子类实现。
Decorator 动态地给对象添加额外的职责。 可能产生大量的装饰器类。 需要动态地给对象添加额外的职责。

总结

Visitor 模式是一种强大的设计模式,它可以让你在不修改现有类结构的前提下,给它们添加新的功能。虽然它有一定的复杂性,但如果使用得当,可以大大提高代码的灵活性和可维护性。记住,任何设计模式都不是银弹,要根据实际情况选择合适的模式。

希望这次的讲解能够帮助大家更好地理解 Visitor 模式。下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注