新谈设计模式 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
| Visitor | Strategy | |
|---|---|---|
| 操作对象 | 多种不同类型的元素 | 一种上下文 |
| 核心手段 | 双重分派 | 单一多态 |
| 扩展方向 | 加新操作容易,加新元素难 | 加新策略容易 |
Visitor vs Iterator
| Visitor | Iterator | |
|---|---|---|
| 关注点 | 对元素做什么操作 | 如何遍历元素 |
| 配合 | 经常配合 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";}}优势:没有虚函数调用开销,编译期检查所有类型必须处理,代码更紧凑。适合元素类型在编译期已知且数量不多的场景。
