首页 > 编程笔记 > C++笔记 阅读:17

访问者模式详解(附带C++实例)

在许多系统中,对象结构相对稳定,例如图形编辑器中的图形对象集合,或者编译器中的抽象语法树。这些结构中的元素可能属于许多不同的类,并且随着产品的发展,我们可能需要对这些对象实施各种操作,如渲染、导出、类型检查或优化等。

如果我们将这些操作直接编码到对象类中,每当添加新操作时,都必须修改这些类。这不仅违反了开闭原则,还使得系统难以管理和扩展。特别是在涉及大量类和操作时,频繁修改是不可行的,也容易引入错误。

访问者模式应运而生,它允许我们在不修改对象结构的情况下引入新的操作。这是通过在外部创建一个或多个访问者类来实现的,这些类可以“访问”对象结构中的元素并对它们执行操作。这种方式的好处是,对象结构的类不需要知道具体的操作细节,只需提供接收访问者的接口,而具体的操作细节则封装在访问者对象中。

访问者模式的引入,使得系统能轻松地应对功能的增加,无须每次变更都修改对象的类定义,尤其当这些功能涉及复杂的决策和操作时。这样,系统的可扩展性和灵活性大大增强,同时也保持了代码的清晰和可维护性。

接下来,我们将深入讨论访问者模式,帮助读者更好地理解访问者模式的实际运用,并看到它在解决某些特定问题时的优势。

访问者模式的核心角色和职责

访问者模式的核心角色及其职责如下:

1) 元素

元素接口声明了一个 accept 方法,该方法接收一个访问者对象作为参数。这是访问者模式中的关键机制,它允许访问者在访问对象结构时能够执行特定的操作。每一个具体元素类都实现了这个接口,并定义了接收访问者的方式,从而使访问者能够对其进行操作。

2) 具体元素

具体元素是实现元素接口的类。

在这些类中,accept 方法的实现通常涉及调用访问者的访问方法、传递自己(即this)作为参数,从而允许访问者访问自己的状态或执行与该元素相关的操作。

3) 访问者

访问者接口声明了一组访问方法,用来处理不同类型的具体元素。这些方法的命名通常反映了它们可以接收的具体元素类型,如 visitConcreteElementA(ConcreteElementA element)。

4) 具体访问者

具体访问者实现了访问者接口,定义了对每种类型的具体元素执行的操作。这使得在不修改元素类的情况下添加新操作成为可能,因为具体的操作逻辑封装在具体访问者类中。

访问者模式如下图所示:


图 1 访问者模式

从图 1 中可以看到,访问者模式在不修改元素类的情况下通过访问者来添加新的操作。核心点就是将操作的实施逻辑从对象结构中分离出来,这使得在不改变元素类的代码的同时,可以灵活地添加新的操作或改变现有操作的实现。

访问者模式的示例应用

假设有一个图形编辑器,其中包含各种图形元素,如圆形、矩形和多边形。我们希望能够在不修改这些图形元素的类的情况下,添加新的操作,如导出图形。
#include <iostream>
#include <vector>
#include <memory>

// 元素接口:所有图形元素都应实现这个接口
class GraphicElement {
public:
    virtual ~GraphicElement() {}
    virtual void accept(class Visitor& v) = 0; // 接收访问者的方法
};

// 访问者接口
class Visitor {
public:
    virtual void visitCircle(class Circle& c) = 0;
    virtual void visitRectangle(class Rectangle& r) = 0;
    virtual void visitPolygon(class Polygon& p) = 0;
};

// 具体元素类:圆形
class Circle : public GraphicElement {
public:
    int radius = 5; // 圆的半径

    void accept(Visitor& v) override {
        v.visitCircle(*this);
    }
};

// 具体元素类:矩形
class Rectangle : public GraphicElement {
public:
    int width = 10;
    int height = 20; // 矩形的宽和高

    void accept(Visitor& v) override {
        v.visitRectangle(*this);
    }
};

// 具体元素类:多边形
class Polygon : public GraphicElement {
public:
    std::vector<std::pair<int, int>> points = {{0, 0}, {5, 10}, {10, 0}}; // 多边形的顶点

    void accept(Visitor& v) override {
        v.visitPolygon(*this);
    }
};

// 具体访问者:导出为svg格式
class SVGExportVisitor : public Visitor {
public:
    void visitCircle(Circle& c) override {
        std::cout << "<svg><circle r=\"" << c.radius << "\"></svg>" << std::endl; // SVG格式输出圆形
    }

    void visitRectangle(Rectangle& r) override {
        std::cout << "<svg><rect width=\"" << r.width << "\" height=\"" << r.height << "\"></svg>" << std::endl;
    }

    void visitPolygon(Polygon& p) override {
        std::cout << "<svg><polygon points=\"";
        for (auto point : p.points) {
            std::cout << point.first << "," << point.second << " ";
        }
        std::cout << "\"></svg>" << std::endl; // SVG格式输出多边形
    }
};

// 主函数:使用访问者模式
int main() {
    std::vector<GraphicElement*> elements; // 图形元素集合
    elements.push_back(new Circle());
    elements.push_back(new Rectangle());
    elements.push_back(new Polygon());

    SVGExportVisitor exportVisitor; // 创建导出访问者

    // 遍历所有元素并接受访问
    for (GraphicElement* element : elements) {
        element->accept(exportVisitor); // 元素接收访问者,进行导出操作
    }

    // 清理资源
    for (GraphicElement* element : elements) {
        delete element;
    }
    elements.clear();

    return 0;
}
这个例子中:
通过这种方式,如果未来需要添加新的导出格式或其他操作,我们只需添加新的访问者类即可,无须改动现有的图形类,从而保持了代码的开闭原则。

访问者模式的应用场景

C++ 程序设计中,除了处理复杂对象结构以外,访问者模式还被用于解决一些特定的问题,其中典型的应用场景包括:

1) 分离算法与对象结构

在需要对一组对象执行操作,而又不希望这些操作使得对象结构变得复杂或臃肿时,访问者模式提供了一种将操作逻辑从数据结构中分离出来的方式。这样可以保持对象结构的稳定性,同时添加新操作而不影响到对象本身。

2) 增加新的操作而非新的类

当系统需要新的操作而不是新的对象类型时,使用访问者模式可以避免修改现有的类结构。这在维护那些需要频繁扩展新功能的大型软件系统中尤为有用,因为它有助于遵守开闭原则。

3) 复用代码

如果不同类型的对象结构中的元素需要执行一些相似或重复的操作,访问者模式可以对这些操作进行集中管理,减少重复代码。通过将操作逻辑封装在访问者中,各种元素的处理方式可以被复用于多个对象结构。

4) 执行复杂的运算

对于需要在一个复杂对象集合上执行复杂运算或策略的情况,访问者模式提供了一种组织代码的有效方法。例如,统计一个文档中不同类型元素的数量,或者在一个游戏的场景图中应用不同的物理效果。

5) 优化设计与分层逻辑

在需要对系统的不同部分应用不同逻辑或者设计层次时,访问者可以帮助实现这一点。例如,在一个多层次的图形用户界面框架中,可以使用访问者来处理事件分发或渲染。

访问者模式的挑战与权衡

虽然访问者模式提供了显著的灵活性和可扩展性,但在实际应用中也面临一些挑战和权衡。
针对上述问题,有以下优化策略:
在应用访问者模式时,C++ 程序员需要注意管理好类型安全和性能开销。因为访问者模式依赖于动态类型识别(通常通过虚函数实现),这可能会引入额外的运行时成本。然而,对于那些结构相对稳定,但需要灵活处理多种操作的系统,访问者模式提供了一种强大的设计策略。

相关文章