访问者模式(Visitor):新操作收进访问者,元素结构不动
更新时间:2026-08-25。本文按"先看不用模式的困境 → 再用模式重构 → 类图对比分析"的结构,论述访问者模式。可运行代码:demos/design-patterns/visitor。
一、问题场景:元素结构稳定,但操作不断新增
语法树上的操作(求值、打印、类型检查、优化、转译)不断增多;图形对象(圆/方/线)的操作(渲染、计算面积、导出 SVG、序列化)不断新增。元素种类稳定,操作维度爆炸。
二、不使用模式:每个新操作给每个元素加一个方法
cpp
// 每个新操作 = 给每个元素类加一个方法
class Circle { void render(); void area(); void serialize(); /* 不断加 */ };
class Rect { void render(); void area(); void serialize(); /* 不断加 */ };
// 加第 4 个操作(导出 SVG)→ 所有元素类都要改
// 违反 OCP:元素类被操作塞满、加操作全量回归| 缺陷 | 说明 |
|---|---|
| 违反 OCP | 加操作 = 改所有元素类 |
| 违反 SRP | 元素类同时承担所有操作 |
| 操作相关代码分散 | 一个操作(如 render)的代码散落所有元素类 |
| 回归面大 | 加操作要全量回归所有元素 |
不使用模式的类图

三、使用模式:访问者集中"一个操作的全部逻辑"
访问者模式用双重分派:每个元素实现 accept(Visitor&),调用 visitor.visit(*this)(第一次分派,运行时绑定到具体元素);访问者重载 visit(Circle&)、visit(Rect&)(第二次分派,绑定到具体操作)。新操作 = 新访问者类,元素类零改动。
cpp
// visitor.h
class Shape;
class Visitor { // 访问者抽象:每种元素一个 visit 重载
public:
virtual void visit(class Circle& c) = 0;
virtual void visit(class Rect& r) = 0;
virtual ~Visitor() = default;
};
class Shape { // 元素抽象
public:
virtual void accept(Visitor& v) = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
public:
void accept(Visitor& v) override { v.visit(*this); } // 第一次分派
double radius_ = 1;
};
class Rect : public Shape {
public:
void accept(Visitor& v) override { v.visit(*this); }
double w_ = 1, h_ = 1;
};
class AreaVisitor : public Visitor { // 一个操作 = 一个访问者
public:
void visit(Circle& c) override { result_ += 3.14 * c.radius_ * c.radius_; }
void visit(Rect& r) override { result_ += r.w_ * r.h_; }
double result_ = 0;
};
class RenderVisitor : public Visitor { /* visit(Circle&) / visit(Rect&) 各自渲染 */ };
// 使用:遍历元素,每个元素 accept(visitor)
for (Shape* s : shapes) s->accept(area);使用模式的类图

四、类图对比与分析
| 维度 | 不使用(元素加方法) | 使用访问者 |
|---|---|---|
| 加新操作 | 改所有元素类 | 新增访问者类 |
| 加新元素 | 只加元素类 | 改所有访问者(加 visit 重载) |
| 操作代码位置 | 分散各元素类 | 集中一个访问者 |
| 元素类职责 | 被所有操作塞满 | 只有 accept(SRP) |
| 双重分派 | 无 | 运行时两次绑定 |
分析结论:访问者把"操作维度"变成可扩展的轴——操作扩展零成本(加访问者),元素扩展高成本(所有访问者要加 visit)。所以它的适用前提是"元素结构稳定、操作频繁增"。代价是理解成本高(双重分派)、破坏封装(访问者常需访问元素内部数据,可用 friend 或公共只读接口缓解)。本仓库性能视角:双重分派 = 两次虚调用,热点路径可测量。
五、适用边界
| 适用 | 不适用 |
|---|---|
| 元素种类稳定、操作频繁新增 | 元素经常新增(访问者改起来痛) |
| 同一操作要作用于不同元素(集中管理) | 操作与元素一对一(直接方法即可) |
| 语法树/对象图遍历处理 | 需要访问私有状态且无法暴露(破坏封装) |
与策略的区别:策略选择"算法"(同类型对象换算法);访问者施加"操作"(不同类型对象被同一次遍历逐个处理)。访问者常与组合模式配合:遍历组合树,每个节点 accept 访问者。
一句话总结
访问者模式用双重分派把"一个操作"集中到访问者类,元素只实现 accept、结构零改动——操作扩展只加访问者(OCP 在操作维度);代价是元素扩展时要改所有访问者、理解成本高,适用于"元素稳定、操作频繁增"的场景。