访问者模式详解(附带C++实例)
在许多系统中,对象结构相对稳定,例如图形编辑器中的图形对象集合,或者编译器中的抽象语法树。这些结构中的元素可能属于许多不同的类,并且随着产品的发展,我们可能需要对这些对象实施各种操作,如渲染、导出、类型检查或优化等。
如果我们将这些操作直接编码到对象类中,每当添加新操作时,都必须修改这些类。这不仅违反了开闭原则,还使得系统难以管理和扩展。特别是在涉及大量类和操作时,频繁修改是不可行的,也容易引入错误。
访问者模式应运而生,它允许我们在不修改对象结构的情况下引入新的操作。这是通过在外部创建一个或多个访问者类来实现的,这些类可以“访问”对象结构中的元素并对它们执行操作。这种方式的好处是,对象结构的类不需要知道具体的操作细节,只需提供接收访问者的接口,而具体的操作细节则封装在访问者对象中。
访问者模式的引入,使得系统能轻松地应对功能的增加,无须每次变更都修改对象的类定义,尤其当这些功能涉及复杂的决策和操作时。这样,系统的可扩展性和灵活性大大增强,同时也保持了代码的清晰和可维护性。
接下来,我们将深入讨论访问者模式,帮助读者更好地理解访问者模式的实际运用,并看到它在解决某些特定问题时的优势。
在这些类中,accept 方法的实现通常涉及调用访问者的访问方法、传递自己(即this)作为参数,从而允许访问者访问自己的状态或执行与该元素相关的操作。
访问者模式如下图所示:

图 1 访问者模式
从图 1 中可以看到,访问者模式在不修改元素类的情况下通过访问者来添加新的操作。核心点就是将操作的实施逻辑从对象结构中分离出来,这使得在不改变元素类的代码的同时,可以灵活地添加新的操作或改变现有操作的实现。
通过这种方式,如果未来需要添加新的导出格式或其他操作,我们只需添加新的访问者类即可,无须改动现有的图形类,从而保持了代码的开闭原则。
针对上述问题,有以下优化策略:
在应用访问者模式时,C++ 程序员需要注意管理好类型安全和性能开销。因为访问者模式依赖于动态类型识别(通常通过虚函数实现),这可能会引入额外的运行时成本。然而,对于那些结构相对稳定,但需要灵活处理多种操作的系统,访问者模式提供了一种强大的设计策略。
如果我们将这些操作直接编码到对象类中,每当添加新操作时,都必须修改这些类。这不仅违反了开闭原则,还使得系统难以管理和扩展。特别是在涉及大量类和操作时,频繁修改是不可行的,也容易引入错误。
访问者模式应运而生,它允许我们在不修改对象结构的情况下引入新的操作。这是通过在外部创建一个或多个访问者类来实现的,这些类可以“访问”对象结构中的元素并对它们执行操作。这种方式的好处是,对象结构的类不需要知道具体的操作细节,只需提供接收访问者的接口,而具体的操作细节则封装在访问者对象中。
访问者模式的引入,使得系统能轻松地应对功能的增加,无须每次变更都修改对象的类定义,尤其当这些功能涉及复杂的决策和操作时。这样,系统的可扩展性和灵活性大大增强,同时也保持了代码的清晰和可维护性。
接下来,我们将深入讨论访问者模式,帮助读者更好地理解访问者模式的实际运用,并看到它在解决某些特定问题时的优势。
访问者模式的核心角色和职责
访问者模式的核心角色及其职责如下:1) 元素
元素接口声明了一个 accept 方法,该方法接收一个访问者对象作为参数。这是访问者模式中的关键机制,它允许访问者在访问对象结构时能够执行特定的操作。每一个具体元素类都实现了这个接口,并定义了接收访问者的方式,从而使访问者能够对其进行操作。2) 具体元素
具体元素是实现元素接口的类。在这些类中,accept 方法的实现通常涉及调用访问者的访问方法、传递自己(即this)作为参数,从而允许访问者访问自己的状态或执行与该元素相关的操作。
3) 访问者
访问者接口声明了一组访问方法,用来处理不同类型的具体元素。这些方法的命名通常反映了它们可以接收的具体元素类型,如 visitConcreteElementA(ConcreteElementA element)。4) 具体访问者
具体访问者实现了访问者接口,定义了对每种类型的具体元素执行的操作。这使得在不修改元素类的情况下添加新操作成为可能,因为具体的操作逻辑封装在具体访问者类中。访问者模式如下图所示:

图 1 访问者模式
从图 1 中可以看到,访问者模式在不修改元素类的情况下通过访问者来添加新的操作。核心点就是将操作的实施逻辑从对象结构中分离出来,这使得在不改变元素类的代码的同时,可以灵活地添加新的操作或改变现有操作的实现。
访问者模式的示例应用
假设有一个图形编辑器,其中包含各种图形元素,如圆形、矩形和多边形。我们希望能够在不修改这些图形元素的类的情况下,添加新的操作,如导出图形。- 定义元素接口:每种图形元素都实现一个 accept 方法,该方法接收一个访问者对象;
- 创建具体元素类:每种图形类(圆形、矩形、多边形)都是元素接口的具体实现;
- 定义访问者接口:访问者接口声明了访问不同图形的方法。
- 实现具体访问者:创建一个导出访问者,它实现了访问者接口并定义了如何将每种图形导出为 SVG 格式。
#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; }这个例子中:
- 每种图形元素(Circle、Rectangle、Polygon)都实现了 GraphicElement 接口,并定义了 accept 方法。accept 方法使得每个元素都能够接收一个访问者(Visitor),并通过调用访问者的相应方法来处理具体的操作,这里是导出为 SVG 格式。
- SVGExportVisitor 是一个具体的访问者,实现了 Visitor 接口,为每种图形定义了导出为 SVG 格式的具体实现。当访问者被传递给图形元素时,元素调用访问者的 visit 方法,从而实现了将图形导出为 SVG 格式的功能,而无须修改图形类本身。
通过这种方式,如果未来需要添加新的导出格式或其他操作,我们只需添加新的访问者类即可,无须改动现有的图形类,从而保持了代码的开闭原则。
访问者模式的应用场景
在 C++ 程序设计中,除了处理复杂对象结构以外,访问者模式还被用于解决一些特定的问题,其中典型的应用场景包括:1) 分离算法与对象结构
在需要对一组对象执行操作,而又不希望这些操作使得对象结构变得复杂或臃肿时,访问者模式提供了一种将操作逻辑从数据结构中分离出来的方式。这样可以保持对象结构的稳定性,同时添加新操作而不影响到对象本身。2) 增加新的操作而非新的类
当系统需要新的操作而不是新的对象类型时,使用访问者模式可以避免修改现有的类结构。这在维护那些需要频繁扩展新功能的大型软件系统中尤为有用,因为它有助于遵守开闭原则。3) 复用代码
如果不同类型的对象结构中的元素需要执行一些相似或重复的操作,访问者模式可以对这些操作进行集中管理,减少重复代码。通过将操作逻辑封装在访问者中,各种元素的处理方式可以被复用于多个对象结构。4) 执行复杂的运算
对于需要在一个复杂对象集合上执行复杂运算或策略的情况,访问者模式提供了一种组织代码的有效方法。例如,统计一个文档中不同类型元素的数量,或者在一个游戏的场景图中应用不同的物理效果。5) 优化设计与分层逻辑
在需要对系统的不同部分应用不同逻辑或者设计层次时,访问者可以帮助实现这一点。例如,在一个多层次的图形用户界面框架中,可以使用访问者来处理事件分发或渲染。访问者模式的挑战与权衡
虽然访问者模式提供了显著的灵活性和可扩展性,但在实际应用中也面临一些挑战和权衡。- 双重分派的复杂性:访问者模式使用了双重分派机制,这可能使得代码结构变得复杂,特别是对于不熟悉此模式的开发者来说。这种机制的使用增加了理解和维护的难度。
- 违反封装:访问者需要访问被访问元素的内部状态,这可能违反对象的封装原则,增加了对象间的耦合。这种设计使得元素的私有数据暴露给外部操作,可能导致数据泄漏和数据不完整的问题。
- 添加新元素困难:虽然添加新的访问者相对容易,但在系统中引入新的具体元素类型则较为困难,因为这需要修改所有现有的访问者以适应新元素。
- 性能考虑:由于访问者模式依赖于运行时的多态性,频繁的虚函数调用可能导致性能损失,特别是在处理大型对象结构时。这种性能开销在大规模系统中尤为明显。
- 上下文依赖:在某些情况下,访问者可能需要访问元素的上下文信息,这可能导致访问者和元素之间紧密耦合。这种依赖关系限制了元素和访问者的独立性,使得系统难以进行模块化设计。
针对上述问题,有以下优化策略:
- 使用静态类型检查:在可能的情况下,使用模板或 CRTP(奇异递归模板模式)来实现静态访问,以减少运行时开销;
- 批处理操作:对于大型对象结构,考虑批量处理元素,而不是逐个处理,以减少函数调用开销;
- 缓存策略:对于频繁访问的结果,可以实现缓存机制,以避免重复计算;
- 权衡封装和性能:在设计时,需要权衡封装性和性能需求,可能的话,提供受控的接口来访问必要的内部状态。
在应用访问者模式时,C++ 程序员需要注意管理好类型安全和性能开销。因为访问者模式依赖于动态类型识别(通常通过虚函数实现),这可能会引入额外的运行时成本。然而,对于那些结构相对稳定,但需要灵活处理多种操作的系统,访问者模式提供了一种强大的设计策略。