新谈设计模式 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 DispatchC 实现#includeiostream#includememory#includestring#includevector#includecmath// 前向声明classCircle;classRectangle;classTriangle;// 访问者接口 classShapeVisitor{public:virtual~ShapeVisitor()default;virtualvoidvisit(constCirclecircle)0;virtualvoidvisit(constRectanglerect)0;virtualvoidvisit(constTriangletri)0;};// 元素接口 classShape{public:virtual~Shape()default;virtualvoidaccept(ShapeVisitorvisitor)const0;// 注意Shape 不需要 draw()、area()、export() 等方法// 这些操作全部放到 Visitor 中};// 具体元素 classCircle:publicShape{public:explicitCircle(doubleradius):radius_(radius){}doubleradius()const{returnradius_;}voidaccept(ShapeVisitorvisitor)constoverride{visitor.visit(*this);// 关键把自己传给 visitor}private:doubleradius_;};classRectangle:publicShape{public:Rectangle(doublew,doubleh):width_(w),height_(h){}doublewidth()const{returnwidth_;}doubleheight()const{returnheight_;}voidaccept(ShapeVisitorvisitor)constoverride{visitor.visit(*this);}private:doublewidth_,height_;};classTriangle:publicShape{public:Triangle(doublebase,doubleheight):base_(base),height_(height){}doublebase()const{returnbase_;}doubleheight()const{returnheight_;}voidaccept(ShapeVisitorvisitor)constoverride{visitor.visit(*this);}private:doublebase_,height_;};// 具体访问者1计算面积 classAreaCalculator:publicShapeVisitor{public:voidvisit(constCirclec)override{doubleareaM_PI*c.radius()*c.radius();totalArea_area;std::cout ○ 圆(rc.radius()) 面积area\n;}voidvisit(constRectangler)override{doublearear.width()*r.height();totalArea_area;std::cout □ 矩形(r.width()×r.height()) 面积area\n;}voidvisit(constTrianglet)override{doublearea0.5*t.base()*t.height();totalArea_area;std::cout △ 三角形(bt.base(),ht.height()) 面积area\n;}doubletotalArea()const{returntotalArea_;}private:doubletotalArea_0;};// 具体访问者2导出 JSON classJSONExporter:publicShapeVisitor{public:voidvisit(constCirclec)override{std::coutR( {type:circle,radius:)c.radius()}\n;}voidvisit(constRectangler)override{std::coutR( {type:rect,width:)r.width()R(,height:)r.height()}\n;}voidvisit(constTrianglet)override{std::coutR( {type:triangle,base:)t.base()R(,height:)t.height()}\n;}};intmain(){// 创建形状集合std::vectorstd::unique_ptrShapeshapes;shapes.push_back(std::make_uniqueCircle(5.0));shapes.push_back(std::make_uniqueRectangle(4.0,6.0));shapes.push_back(std::make_uniqueTriangle(3.0,8.0));shapes.push_back(std::make_uniqueCircle(2.0));// 访问者1计算面积std::cout 计算面积 \n;AreaCalculator areaCalc;for(constautoshape:shapes){shape-accept(areaCalc);// 双重分派}std::cout总面积: areaCalc.totalArea()\n;// 访问者2导出 JSON不需要改任何 Shape 代码std::cout\n 导出 JSON \n;JSONExporter jsonExporter;for(constautoshape:shapes){shape-accept(jsonExporter);}}输出 计算面积 ○ 圆(r5) 面积78.5398 □ 矩形(4×6) 面积24 △ 三角形(b3,h8) 面积12 ○ 圆(r2) 面积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 StrategyVisitorStrategy操作对象多种不同类型的元素一种上下文核心手段双重分派单一多态扩展方向加新操作容易加新元素难加新策略容易Visitor vs IteratorVisitorIterator关注点对元素做什么操作如何遍历元素配合经常配合 Iterator 使用提供元素给 Visitor现代 C 替代方案std::variantstd::visitC17 提供了内置的访问者模式支持#includevariant#includevector#includecmathstructCircle{doubleradius;};structRect{doublew,h;};// variant 可以持有 Circle 或 Rect 中的任一种usingShapestd::variantCircle,Rect;// overloaded 辅助模板——让多个 lambda 合并成一个可调用对象// 这是 C17 的常见技巧// 1. 继承所有传入的 lambda 类型// 2. 用 using Ts::operator()... 把它们的 operator() 全部暴露出来// 3. std::visit 会根据 variant 里实际存的类型调用匹配的那个 lambdatemplateclass...Tsstructoverloaded:Ts...{usingTs::operator()...;};// 推导指南C17 CTAD让编译器自动推导模板参数templateclass...Tsoverloaded(Ts...)-overloadedTs...;intmain(){std::vectorShapeshapes{Circle{5},Rect{4,6},Circle{2}};for(constautoshape:shapes){// std::visit 根据 variant 实际类型分派到对应的 lambdadoubleareastd::visit(overloaded{[](constCirclec){returnM_PI*c.radius*c.radius;},[](constRectr){returnr.w*r.h;},// 如果你漏了某个类型的 lambda编译直接报错// 这比虚函数版本安全——虚函数版忘写一个 visit 重载只会在运行时出问题},shape);std::cout面积: area\n;}}优势没有虚函数调用开销编译期检查所有类型必须处理代码更紧凑。适合元素类型在编译期已知且数量不多的场景。