C++ Visitor设计模式怎么用?如何实现访问者模式遍历对象结构?

文章导读
Previous Quiz Next 访问者设计模式 是一种编写代码的方式,它帮助你在不反复修改这些 class 的情况下,为它们添加新功能或新类型的工作。
📋 目录
  1. 访问者模式组件
  2. C++ 中的访问者模式实现
  3. Visitor 设计模式的优点和缺点
  4. Visitor 设计模式的实际应用
  5. Conclusion
A A

C++ 中的访问者设计模式



Previous
Quiz
Next

访问者设计模式 是一种编写代码的方式,它帮助你在不反复修改这些 class 的情况下,为它们添加新功能或新类型的工作。

在编程中,我们经常创建代表真实事物的 class,比如形状、物品、文件、动物或其他任何东西。这些 class 通常包含一些基本数据,可能还有一两个简单 function。但随着项目的发展,我们希望对这些 class 执行越来越多操作。如果我们继续将所有新操作直接添加到这些 class 中,这些 class 会变得非常庞大且难以阅读。访问者模式帮助我们避免 class 变得杂乱。

想象一座学校大楼。学校管理员无法修理电线、修水管、清洁教室或检查家具。相反,不同的人会来学校做这些工作。电工来修电线,水管工来修水管,清洁工来清洁房间,木匠来修家具。学校管理员允许不同的工人进入并完成他们的工作。

Visitor Design Pattern Illustration

这正是访问者模式的核心思想。你的对象就像学校大楼,visitor 就像前来工作的工人。大楼有一个门,对象有一个 accept 方法。visitor 通过 accept 方法进入,然后执行所需的工作。

当你有很多不同对象,并且希望不断为这些对象添加新任务时,就会使用这种模式。如果你试图将每个任务都放入对象 class 中,这些 class 会变得过大。访问者模式通过将工作移到独立的 visitor class 中来保持代码整洁。

访问者模式组件

以下是访问者设计模式的主要组件 −

  • Visitor Interface − 这是一个计划,指定 visitor 可以处理哪些类型的对象。它为每种元素类型提供一个 visit function。例如,visitCircle()、visitSquare()、visitRectangle()。
  • Concrete Visitor − 这些是实际执行工作的 class。例如,AreaCalculator visitor 会计算面积,PerimeterCalculator visitor 会计算周长,DrawingVisitor 可能在屏幕上绘制形状。
  • Element Interface − 这是所有元素必须遵循的主要接口。它通常只包含一个 function:accept(visitor)。每个 concrete element 都必须实现它。
  • Concrete Element − 这些是你的实际对象。例如,Circle、Square 和 Rectangle。 它们存储简单数据如半径或边长,并实现 accept 方法。
  • Object Structure − 这通常是一个元素列表或集合。visitor 会遍历这个结构,逐一访问每个元素。
  • Client − client 负责设置一切。它创建形状、创建 visitor,并让元素接受 visitor。

C++ 中的访问者模式实现

让我们使用前面提到的形状示例,在 C++ 中实现访问者设计模式。我们将创建一个用于计算面积和周长的 visitor interface,以及针对每个操作的具体 visitor 类。

实现访问者设计模式的步骤

下图展示了实现访问者设计模式的步骤。

Steps to Implement Visitor Design Pattern
  • 首先,创建一个 Visitor interface。这个接口将为每种元素类型提供一个 visit 函数。例如,visitCircle、visitSquare、visitRectangle。这告诉 visitor 它可以处理哪些类型的对象。
  • 然后创建 Concrete Visitor classes。这些是真正的工作者。每个 visitor 类负责一种特定工作,比如计算面积、周长、打印详细信息、绘制形状,或者其他任何你需要的操作。在每个 visitor 类中,你编写访问每种元素类型时要执行的操作。
  • 之后,创建一个 Element interface。这个接口只有一个重要的函数:accept。每个元素(如 Circle、Square、Rectangle)都必须实现这个接口。这个 accept 函数是 visitor 进入的“门”。
  • 接下来,创建 Concrete Element classes。这些是你的实际对象。例如,Circle 有半径,Square 有边长,Rectangle 有宽度和高度。每个类都必须实现 accept 函数。在 accept 中,元素只需调用 visitor 对应的 visit 方法并传递自身。
  • 创建一个 Object Structure。这只是一个集合(如 vector、list 或 array),用于容纳所有元素。visitor 将遍历这个结构,逐一访问每个元素。这使得同时对多个元素运行 visitor 变得非常容易。
  • 最后,编写 Client code。客户端创建对象,将它们放入结构中,并创建 visitor。然后客户端对每个元素调用 accept 并传递 visitor。这将启动整个过程,让 visitor 对每个元素执行其工作。

访问者设计模式的 C++ 代码

在这个示例中,我们有一个抽象类 Shape,它定义了形状如 CircleSquareRectangle 的接口。Visitor 接口声明了针对每种形状类型的 visit 方法。具体 visitor 类 AreaCalculatorPerimeterCalculator 分别实现了 visit 方法来计算面积和周长。

#include <iostream>
#include <vector>
#include <cmath>
using namespace std;

// 前置声明
class Circle;
class Square;
class Rectangle;
// Visitor 接口
class Visitor {
   public:
      virtual void visit(Circle* c) = 0;
      virtual void visit(Square* s) = 0;
      virtual void visit(Rectangle* r) = 0;
};

// Element 接口
class Shape {
   public:
      virtual void accept(Visitor* v) = 0;
};

// 具体元素:Circle
class Circle : public Shape {
   private:
      double radius;
   public:
      Circle(double r) : radius(r) {}
      double getRadius() { return radius; }
      void accept(Visitor* v) override {
         v->visit(this);
      }
};

// 具体元素:Square
class Square : public Shape {
   private:
      double side;
   public:
      Square(double s) : side(s) {}
      double getSide() { return side; }
      void accept(Visitor* v) override {
         v->visit(this);
      }
};

// 具体元素:Rectangle
class Rectangle : public Shape {
   private:
      double width, height;
   public:
      Rectangle(double w, double h) : width(w), height(h) {}
      double getWidth() { return width; }
      double getHeight() { return height; }
      void accept(Visitor* v) override {
         v->visit(this);
      }
};

// 具体 Visitor:AreaCalculator
class AreaCalculator : public Visitor {
   public:
      void visit(Circle* c) override {
         double area = M_PI * c->getRadius() * c->getRadius();
         cout << "Area of Circle: " << area << endl;
      }
      void visit(Square* s) override {
         double area = s->getSide() * s->getSide();
         cout << "Area of Square: " << area << endl;
      }
      void visit(Rectangle* r) override {
         double area = r->getWidth() * r->getHeight();
         cout << "Area of Rectangle: " << area << endl;
      }
};

// 具体 Visitor:PerimeterCalculator
class PerimeterCalculator : public Visitor {
   public:
      void visit(Circle* c) override {
         double perimeter = 2 * M_PI * c->getRadius();
         cout << "Perimeter of Circle: " << perimeter << endl;
      }
      void visit(Square* s) override {
         double perimeter = 4 * s->getSide();
         cout << "Perimeter of Square: " << perimeter << endl;
      }
      void visit(Rectangle* r) override {
         double perimeter = 2 * (r->getWidth() + r->getHeight());
         cout << "Perimeter of Rectangle: " << perimeter << endl;
      }
};

// 客户端代码
int main() {
   vector<Shape*> shapes;
   shapes.push_back(new Circle(5));
   shapes.push_back(new Square(4));
   shapes.push_back(new Rectangle(3, 6));

   AreaCalculator areaCalc;
   PerimeterCalculator perimeterCalc;

   for (Shape* shape : shapes) {
      shape->accept(&areaCalc);
      shape->accept(&perimeterCalc);
   }

   // 清理资源
   for (Shape* shape : shapes) {
      delete shape;
   }

   return 0;
}

以下是上述代码的输出 −

Area of Circle: 78.5398
Perimeter of Circle: 31.4159
Area of Square: 16
Perimeter of Square: 16
Area of Rectangle: 18
Perimeter of Rectangle: 18

输出打印了每个形状的面积和周长。这里重要的是,形状本身并没有计算这些值。所有工作都是由 visitor 完成的,形状只是允许 visitor 进入。

Visitor 设计模式的优点和缺点

以下是使用 visitor 设计模式的优点和缺点 −

优点 缺点
以后容易添加新工作。你可以随时创建新的 visitor,而无需修改旧的类。 如果你添加新元素类型,必须更新每个 visitor,这可能会很烦人。
保持元素类小巧。它们只包含数据和一个accept 函数。 初次理解需要时间,因为工作不在元素内部,而是在visitor内部。
当需要在同一组对象上执行多种不同任务时非常合适。每个任务都保存在自己的visitor中。 会创建许多visitor 类,如果不保持组织,项目可能会显得庞大。
使项目更容易维护,因为主要类很少变化。这减少了错误 如果元素的形状或结构经常变化,visitor 就容易崩溃

Visitor 设计模式的实际应用

以下是 visitor 设计模式在实际软件中的用例 −

  • 编译器 − 编译器经常使用 visitor 模式,因为它们需要遍历语法树中的多种不同类型的节点。这些节点代表数字、变量、循环、条件、函数等内容。与其在每个节点类中放入检查、打印、优化和代码生成等逻辑,不如使用 visitor。这样可以轻松向编译器添加新的 pass,而无需修改原始树结构。
  • 文档处理 − 在文档编辑器中,可能有文本块、图像、表格、链接、形状等项目。你可能想要打印文档、导出为 PDF、统计字数、分析样式、高亮错误,或转换为其他格式。与其将所有这些任务放入文档元素中,不如让每个任务成为一个 visitor。这保持了文档类的简洁,并允许以后添加新功能。
  • 图形和游戏开发 − 游戏中有许多对象,如玩家、敌人、子弹、墙壁、物品和环境对象。你可能想要以不同方式渲染它们、检查碰撞、更新物理效果,或调试其状态。这些操作中的每一种都可以是一个 visitor。这使游戏代码更清晰,因为你避免了在每个游戏对象中塞入过多函数。visitor 方法还有助于运行不同的“pass”,如更新生命值、检查触发器,或为调试绘制轮廓。
  • 数据结构 − 树、图和文件系统是使用 visitor 模式的最佳场所。 这些结构通常包含多种节点类型,你需要执行搜索、排序、打印、导出、计算大小、检查规则或验证数据等操作。visitor 使运行所有这些操作变得容易,而无需重写结构代码。
  • 金融系统 − 在金融领域,可能有多种金融工具,如贷款、债券、股票、保险计划或交易。你可能想要计算利息、总价值、税费、风险分数,或生成报告。使用 visitor 允许将这些操作分别编写。这也有助于金融公司更新公式或添加新型计算,而无需编辑核心类。
  • 分析和报告工具 − 许多仪表板和分析工具处理图表、图形、指标、日志和事件等对象。visitor 有助于执行生成周报、导出数据、汇总使用情况、清理日志,或将数据转换为新格式等任务。
  • IDE 工具和代码编辑器 − 当编辑器分析你的代码以显示错误、警告、自动建议、重构选项时,它经常使用 visitor 遍历你的程序结构。 每个功能都成为一个 visitor。
Real-world Software Use-cases of Visitor Design Pattern

Conclusion

在本章中,我们探讨了 visitor design pattern,这是一种强大的 behavioral design pattern,它允许将算法与它们操作的对象分离。我们讨论了该 pattern 的关键组件,包括 visitor interface、concrete visitors、element interface 和 concrete elements。我们还使用一个 shape 示例在 C++ 中实现了 visitor pattern,演示了如何使用单独的 visitor class 来计算面积和周长。最后,我们考察了 visitor pattern 的优缺点,并突出了其在各种领域的实际应用。通过使用 visitor design pattern,开发者可以提升代码的可维护性,促进关注点分离,并轻松向现有对象结构添加新操作。