当前位置: 首页 > news >正文

新谈设计模式 Chapter 22 — 访问者模式 Visitor

Chapter 22 — 访问者模式 Visitor

灵魂速记:体检——不同科室的医生,检查同一个人的不同部位。人不变,检查方式随便加。


秒懂类比

你去医院体检:

  • 内科医生来了,给你量血压、听心肺
  • 眼科医生来了,给你测视力
  • 牙科医生来了,给你检查牙齿

你(数据结构)没变,但来了不同的医生(访问者),对你做不同的操作。以后加一个"皮肤科"?加一个医生就行,你不用改。


问题引入

// 灾难现场:给形状类不断添加操作classShape{virtualvoiddraw()=0;// 第一天的需求virtualdoublearea()=0;// 第二天加的virtualvoidexportXML()=0;// 第三天加的virtualvoidexportJSON()=0;// 第四天加的virtualvoidprint()=0;// 第五天加的……// Shape 类越来越胖,每加一种操作,所有子类都要改};

问题:每次加新操作,要改 Shape + Circle + Rectangle + Triangle……所有类。

反转思路:能不能不改类,把新操作放到外面?


模式结构

┌─────────────┐ ┌─────────────┐ │ Element │ │ Visitor │ ├─────────────┤ ├─────────────┤ │+accept(v) { │ │+visitCircle │ │ v.visit(this)│ ←──────│+visitRect │ │} │ 双重 │+visitTriangle│ └──────┬──────┘ 分派 └──────┬──────┘ │ │ ┌────┴────┐ ┌─────┴─────┐ │Circle │ │AreaCalc │ │Rect │ │XMLExporter │ │Triangle │ │JSONExporter│ └─────────┘ └───────────┘ 元素稳定不变 操作可以随意添加

关键词:双重分派(Double Dispatch)


C++ 实现

#include<iostream>#include<memory>#include<string>#include<vector>#include<cmath>// 前向声明classCircle;classRectangle;classTriangle;// ========== 访问者接口 ==========classShapeVisitor{public:virtual~ShapeVisitor()=default;virtualvoidvisit(constCircle&circle)=0;virtualvoidvisit(constRectangle&rect)=0;virtualvoidvisit(constTriangle&tri)=0;};// ========== 元素接口 ==========classShape{public:virtual~Shape()=default;virtualvoidaccept(ShapeVisitor&visitor)const=0;// 注意:Shape 不需要 draw()、area()、export() 等方法// 这些操作全部放到 Visitor 中};// ========== 具体元素 ==========classCircle:publicShape{public:explicitCircle(doubleradius):radius_(radius){}doubleradius()const{returnradius_;}voidaccept(ShapeVisitor&visitor)constoverride{visitor.visit(*this);// 关键:把自己传给 visitor}private:doubleradius_;};classRectangle:publicShape{public:Rectangle(doublew,doubleh):width_(w),height_(h){}doublewidth()const{returnwidth_;}doubleheight()const{returnheight_;}voidaccept(ShapeVisitor&visitor)constoverride{visitor.visit(*this);}private:doublewidth_,height_;};classTriangle:publicShape{public:Triangle(doublebase,doubleheight):base_(base),height_(height){}doublebase()const{returnbase_;}doubleheight()const{returnheight_;}voidaccept(ShapeVisitor&visitor)constoverride{visitor.visit(*this);}private:doublebase_,height_;};// ========== 具体访问者1:计算面积 ==========classAreaCalculator:publicShapeVisitor{public:voidvisit(constCircle&c)override{doublearea=M_PI*c.radius()*c.radius();totalArea_+=area;std::cout<<" ○ 圆(r="<<c.radius()<<") 面积="<<area<<"\n";}voidvisit(constRectangle&r)override{doublearea=r.width()*r.height();totalArea_+=area;std::cout<<" □ 矩形("<<r.width()<<"×"<<r.height()<<") 面积="<<area<<"\n";}voidvisit(constTriangle&t)override{doublearea=0.5*t.base()*t.height();totalArea_+=area;std::cout<<" △ 三角形(b="<<t.base()<<",h="<<t.height()<<") 面积="<<area<<"\n";}doubletotalArea()const{returntotalArea_;}private:doubletotalArea_=0;};// ========== 具体访问者2:导出 JSON ==========classJSONExporter:publicShapeVisitor{public:voidvisit(constCircle&c)override{std::cout<<R"( {"type":"circle","radius":)"<<c.radius()<<"}\n";}voidvisit(constRectangle&r)override{std::cout<<R"( {"type":"rect","width":)"<<r.width()<<R"(,"height":)"<<r.height()<<"}\n";}voidvisit(constTriangle&t)override{std::cout<<R"( {"type":"triangle","base":)"<<t.base()<<R"(,"height":)"<<t.height()<<"}\n";}};intmain(){// 创建形状集合std::vector<std::unique_ptr<Shape>>shapes;shapes.push_back(std::make_unique<Circle>(5.0));shapes.push_back(std::make_unique<Rectangle>(4.0,6.0));shapes.push_back(std::make_unique<Triangle>(3.0,8.0));shapes.push_back(std::make_unique<Circle>(2.0));// 访问者1:计算面积std::cout<<"=== 计算面积 ===\n";AreaCalculator areaCalc;for(constauto&shape:shapes){shape->accept(areaCalc);// 双重分派!}std::cout<<"总面积: "<<areaCalc.totalArea()<<"\n";// 访问者2:导出 JSON(不需要改任何 Shape 代码!)std::cout<<"\n=== 导出 JSON ===\n";JSONExporter jsonExporter;for(constauto&shape:shapes){shape->accept(jsonExporter);}}

输出:

=== 计算面积 === ○ 圆(r=5) 面积=78.5398 □ 矩形(4×6) 面积=24 △ 三角形(b=3,h=8) 面积=12 ○ 圆(r=2) 面积=12.5664 总面积: 127.106 === 导出 JSON === {"type":"circle","radius":5} {"type":"rect","width":4,"height":6} {"type":"triangle","base":3,"height":8} {"type":"circle","radius":2}

双重分派的秘密

为什么叫"双重分派"?因为最终调用的方法取决于两个对象的类型

shape->accept(visitor);// 第一次分派:根据 shape 的实际类型(Circle),调用 Circle::accept// Circle::accept(visitor) { visitor.visit(*this); }// 第二次分派:根据 visitor 的实际类型(AreaCalculator),调用 AreaCalculator::visit(Circle&)// 两次虚函数调用 → 同时根据 shape 和 visitor 的类型决定行为

C++ 不直接支持多重分派,Visitor 模式用两次单分派模拟了双重分派。


什么时候用?

✅ 适合❌ 别用
数据结构(元素类型)稳定不变经常添加新的元素类型
操作经常变化(要加新操作)操作固定不变
想把数据结构和操作分离操作和数据结构天然一体
编译器 AST 遍历、文档处理简单场景(过度设计)

⚠️Visitor 的软肋:如果要加新的元素类型(比如加个 Pentagon),所有 Visitor 子类都要改。它擅长加"操作",不擅长加"元素"。


防混淆

Visitor vs Strategy

VisitorStrategy
操作对象多种不同类型的元素一种上下文
核心手段双重分派单一多态
扩展方向加新操作容易,加新元素难加新策略容易

Visitor vs Iterator

VisitorIterator
关注点对元素做什么操作如何遍历元素
配合经常配合 Iterator 使用提供元素给 Visitor

现代 C++ 替代方案:std::variant+std::visit

C++17 提供了内置的访问者模式支持:

#include<variant>#include<vector>#include<cmath>structCircle{doubleradius;};structRect{doublew,h;};// variant 可以持有 Circle 或 Rect 中的任一种usingShape=std::variant<Circle,Rect>;// overloaded 辅助模板——让多个 lambda 合并成一个可调用对象// 这是 C++17 的常见技巧:// 1. 继承所有传入的 lambda 类型// 2. 用 using Ts::operator()... 把它们的 operator() 全部暴露出来// 3. std::visit 会根据 variant 里实际存的类型,调用匹配的那个 lambdatemplate<class...Ts>structoverloaded:Ts...{usingTs::operator()...;};// 推导指南(C++17 CTAD),让编译器自动推导模板参数template<class...Ts>overloaded(Ts...)->overloaded<Ts...>;intmain(){std::vector<Shape>shapes={Circle{5},Rect{4,6},Circle{2}};for(constauto&shape:shapes){// std::visit 根据 variant 实际类型,分派到对应的 lambdadoublearea=std::visit(overloaded{[](constCircle&c){returnM_PI*c.radius*c.radius;},[](constRect&r){returnr.w*r.h;},// 如果你漏了某个类型的 lambda,编译直接报错!// 这比虚函数版本安全——虚函数版忘写一个 visit 重载只会在运行时出问题},shape);std::cout<<"面积: "<<area<<"\n";}}

优势:没有虚函数调用开销,编译期检查所有类型必须处理,代码更紧凑。适合元素类型在编译期已知且数量不多的场景。

http://www.jsqmd.com/news/716819/

相关文章:

  • 别再只会用Excel了!用Minitab做控制图,5分钟搞定SPC分析(附实战数据)
  • POLIR-Laws-国家赔偿: 《中华人民共和国国家赔偿法》
  • Docker AI Toolkit 2026架构图首度泄露(含Control Plane与AI Runtime双平面通信协议),仅开放48小时下载
  • 开源闪电探测器Flash Bee:低成本DIY雷电预警方案
  • C++27 std::atomic_ref正式落地:3大编译器(GCC 14/Clang 18/MSVC 19.42)生成汇编级对比,性能跃升42%的关键配置
  • 新手入门:三步、四步相移算法到底怎么选?一个实验帮你搞定(附MATLAB/Python代码)
  • php内核 海外冗余模块裁剪、无用组件移除方案
  • Gems 捷迈 FT-110 工业级涡轮式低流量传感器的国产替代方案
  • 答辩 PPT 不用熬!虎贲等考 AI PPT:论文一键生成,学术风直接过关
  • MFA(多重身份验证)绕过码风险解析,如何管控MFA绕过风险,筑牢身份认证防线
  • 5步深度优化:Win11Debloat终极系统清理与性能提升指南
  • UART协议避坑指南:波特率、采样与多数表决,你的串口通信稳定吗?
  • LFM2-2.6B-GGUF在运维自动化中的应用:智能解析日志并执行故障修复脚本
  • 告别混乱:PCIe 6.0的Shared Buffer用Credit Block实现了怎样的秩序?
  • 别再只盯着ICP了!用PCL实战计算点云配准的RMSE与重合率(附完整C++代码)
  • Playwright MCP终极指南:AI驱动的浏览器自动化革命
  • Steam Deck终极插件指南:5分钟解锁Decky Loader的全部潜力
  • springboot+vue3宠物领养系统 原生微信小程序
  • 【小白轻松解决】龙虾智能体 2.6.4 一键安装完整教程(内含安装包)
  • 施耐德Pro-face远程客户端避坑指南:独家触控和状态图标设置,防止产线误操作
  • 熙瑾会悟离线转记踩坑实录:实时纠错 SDK 适配问题深度排查与解决方案
  • 国风美学生成模型v1.0社区共建:如何参与开源项目并贡献Prompt案例
  • 给应用层开发者的AutoSar BSW避坑指南:别再被MCAL、ECU抽象层搞晕了
  • 如何利用客流数据优化零售店转化率?基于“经过人数”和“停留人数”的数据驱动优化模型
  • 【国产AI推理引擎集成实战指南】:Java开发者必看的3大国产化替代方案与性能对比数据
  • 全球首个GPU加速5G Open RAN技术解析与应用
  • Qwen3-VL论文精读
  • C++中继承的概念和定义
  • 90K参数轻量模型实战:在Windows笔记本上跑通IAT暗光增强(含LOL数据集处理避坑指南)
  • 告别JTAG烧录器:用MCU模拟JTAG接口,低成本搞定安路FPGA/CPLD远程更新