news 2026/4/29 2:01:55

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
新谈设计模式 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";}}

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/29 2:01:24

spring-boot-starter-validation字段数据校验

validation 概述 spring-boot-starter-validation 是 Spring Boot 官方提供的用于数据校验的启动器&#xff0c;它基于 Bean Validation API (JSR 380) 标准&#xff0c;并默认使用 Hibernate Validator 作为其实现。这个框架能让你通过声明式的注解&#xff0c;轻松地对控制器…

作者头像 李华
网站建设 2026/4/29 1:58:42

5步深度优化:Win11Debloat终极系统清理与性能提升指南

5步深度优化&#xff1a;Win11Debloat终极系统清理与性能提升指南 【免费下载链接】Win11Debloat A simple, lightweight PowerShell script that allows you to remove pre-installed apps, disable telemetry, as well as perform various other changes to declutter and cu…

作者头像 李华
网站建设 2026/4/29 1:56:58

【限时首发|Docker官方认证架构师亲授】:2026版Toolkit如何实现「零配置多模态训练容器化」?附可运行的架构验证代码库

更多请点击&#xff1a; https://intelliparadigm.com 第一章&#xff1a;Docker AI Toolkit 2026 发布背景与核心定位 随着大模型本地化推理、边缘AI训练和多模态工作流编排需求激增&#xff0c;容器化AI开发正从“可选实践”演进为“工程刚需”。Docker AI Toolkit 2026 应运…

作者头像 李华
网站建设 2026/4/29 1:56:04

PyTorch + TensorBoard 超实用笔记:从零开始监控你的模型训练

TensorBoard 是深度学习训练中必不可少的“仪表盘”。它可以实时展示损失曲线、准确率变化、模型结构、参数分布&#xff0c;甚至图像和音频。 好消息是&#xff1a;PyTorch 原生支持 TensorBoard&#xff0c;装一个包就能用&#xff0c;完全不用写 TensorFlow 代码。本文会带你…

作者头像 李华