设计模式之访问者模式
阅读指引:本文较长,可按需跳读。
- 想快速理解 → 读「概念」「举例理解」「双重分派:访问者模式的灵魂」;
- 想看代码 → 读「代码示例」(含 UML、Java、C++);
- 想区分相似模式 → 读「访问者模式与策略模式、迭代器模式的对比」;
- 想判断该不该用 → 读「优缺点」「一个反面例子」「应用场景」;
- 准备落地 → 读「注意事项」与「访问者模式不负责什么」。
概念
定义
访问者模式(Visitor Pattern) 是一种行为型设计模式。它的核心思想是:
将作用于某种对象结构中各元素的操作分离出来,封装到一个独立的“访问者”对象中,使得在不修改各元素类的前提下,可以定义作用于这些元素的新操作。
换句话说:当你的对象结构(元素类型)稳定,但施加于其上的操作经常变化时,把“操作”从“元素”中请出去,让元素只负责接受访问,操作由外部的访问者来承载。
结构
访问者模式通常有五个角色:
-
Visitor 访问者接口
- 为对象结构中每一种具体元素类型声明一个
visit(ConcreteElement)方法。 - 方法个数通常等于元素类型的个数。
- 为对象结构中每一种具体元素类型声明一个
-
ConcreteVisitor 具体访问者
- 实现
Visitor接口,为每种元素提供该操作的具体算法。 - 例如:“薪资计算访问者”为工程师、产品经理、管理者分别计算薪资。
- 实现
-
Element 元素接口
- 声明一个
accept(Visitor)方法。 - 这是访问者模式对元素类的唯一侵入点。
- 声明一个
-
ConcreteElement 具体元素
- 实现
accept(Visitor visitor),通常只有一行:visitor.visit(this);。 - 元素将自身作为参数回传给访问者,完成第二次分派。
- 实现
-
ObjectStructure 对象结构
- 持有一组元素,负责遍历它们并将访问者传递给每个元素。
- 可以是集合、树、图等任何聚合结构。
举例理解
假设你维护一个员工管理系统。员工类型很稳定,只有三种:工程师、产品经理、管理者。但业务部门不断提出新的统计需求:算工资、算绩效、算人力成本、算培训预算……
不用访问者模式时,直觉做法是把每个统计方法都加到员工类上:
abstract class Employee {
abstract double computeSalary();
abstract double computePerformance();
abstract double computeCost();
// 未来还会有 computeTrainingBudget()、computeReport()……
}
每来一个新统计需求,你就要同时改三个子类。统计逻辑散落在各个员工类里,员工类越来越臃肿,新人不敢动。
访问者模式的解法:
- 员工类(Element)只保留与员工自身最相关的行为,外加一个
accept(Visitor)方法。 - 每一种统计操作(薪资、绩效、成本……)被封装成一个独立的
Visitor实现。 - 新增统计需求时,只新增一个 Visitor 类,工程师、产品经理、管理者三个元素类一行都不用改。
用一个更生活化的类比:
访问者模式就像不同身份的人去同一栋楼里办事。 楼里的房间(元素)是固定的:财务室、人事室、IT 室。但来访的人(访问者)不同,在每个房间里做的事也不同:审计员在财务室查账,在人事室查考勤;新员工在财务室领工资卡,在人事室办入职。房间本身不需要知道“谁会来、来干什么”,它只需要“开门,让访问者进来做事”。新增一种来访身份(新的 Visitor),楼里的房间结构完全不用动。
这个类比的关键点:元素结构稳定,操作方(访问者)多变——这正是访问者模式的适用前提。
双重分派:访问者模式的灵魂
如果只记住访问者模式“把操作抽出去”,而不理解 双重分派(Double Dispatch) ,就等于没真正理解它。
什么是单分派
在 Java、C++、C# 等主流面向对象语言中,方法调用通常是单分派的:运行时选择哪个方法,只取决于接收者的运行时类型(this 的类型),参数类型在编译期就已经静态确定。
考虑下面的场景:
abstract class Employee { }
class Engineer extends Employee { }
class Manager extends Employee { }
class Visitor {
void visit(Engineer e) { System.out.println("处理工程师"); }
void visit(Manager m) { System.out.println("处理管理者"); }
}
如果你写 Employee e = new Engineer(); visitor.visit(e);,Java 会在编译期根据参数的静态类型 Employee 来选择重载版本。如果 Visitor 中没有 visit(Employee),编译就无法通过;即使有,也不会根据 e 的实际类型 Engineer 去调用 visit(Engineer)。参数类型的运行时信息,在单分派的重载解析中不起作用。
访问者如何实现双重分派
访问者模式通过两次虚方法调用,把“元素类型”和“访问者类型”都纳入运行时决策:
第一次分派:element.accept(visitor)。accept 是元素接口的虚方法,实际调用哪个 accept,取决于元素的运行时类型。
第二次分派:在具体元素的 accept 内部,执行 visitor.visit(this)。注意这里的 this 是具体元素类型(因为 accept 已经落在具体类里了)。visit 是访问者接口的虚方法,实际调用哪个 visit,取决于访问者的运行时类型,同时参数类型已经由 this 确定为具体元素类型。
最终执行的 visit 方法,同时由“元素的运行时类型”和“访问者的运行时类型”共同决定。这就是双重分派。
用一段最小代码把这条链路看清楚:
interface Visitor {
void visit(Engineer e);
void visit(Manager m);
}
abstract class Employee {
abstract void accept(Visitor v);
}
class Engineer extends Employee {
void accept(Visitor v) {
v.visit(this); // this 的静态类型已是 Engineer,第二次分派命中 visit(Engineer)
}
}
class Manager extends Employee {
void accept(Visitor v) {
v.visit(this); // 命中 visit(Manager)
}
}
当 Employee e = new Engineer(); e.accept(new SalaryVisitor()); 执行时:
e.accept(...)动态绑定到Engineer.accept;Engineer.accept中的this是Engineer类型,v.visit(this)编译时即绑定到visit(Engineer)这个重载;v的运行时类型是SalaryVisitor,虚分派落到SalaryVisitor.visit(Engineer)。
没有访问者模式,你需要 instanceof 或反射来达到同样的效果;访问者模式用纯多态替代了这些运行时类型检查。
一句话:单分派只问“你是谁”(接收者);双重分派同时问“你是谁”和“来访的是谁”。访问者模式是用两次单分派模拟双重分派的经典技巧。
工作流程
- 元素侧:每个具体元素实现
accept(Visitor visitor),方法体几乎总是visitor.visit(this);。 - 访问者侧:每个具体访问者实现
Visitor接口,为每一种元素类型提供自己的处理逻辑。 - 对象结构侧:遍历持有的元素,对每个元素调用
accept(visitor)。 - 双重分派发生:
accept的动态绑定根据元素运行时类型选中具体元素的accept;其中的visit(this)再根据访问者运行时类型选中具体访问者的对应重载。 - 操作完成:访问者在
visit方法中拿到元素对象,通过元素的公开接口读取所需数据,执行该访问者所代表的业务逻辑。
一句话概括:
元素开门(accept),访问者进来做事(visit);做什么由访问者决定,对谁做由元素决定。
代码示例
UML
以“员工统计”为例:元素是 Engineer、ProductManager、Manager;访问者是“薪资计算”和“成本分析”。
| 角色 | 类 | 职责 |
|---|---|---|
| Visitor | Visitor | 为每种元素声明 visit 方法 |
| ConcreteVisitor | SalaryVisitor、CostVisitor | 薪资算法、成本算法 |
| Element | Employee | 声明 accept,提供读取数据的方法 |
| ConcreteElement | Engineer、ProductManager、Manager | 实现 accept,将自身交给访问者 |
| ObjectStructure | EmployeeStructure | 遍历员工,把访问者分发给每个元素 |
Java 实现
import java.util.ArrayList;
import java.util.List;
// ---------- 1. 访问者接口 ----------
interface Visitor {
void visit(Engineer e);
void visit(ProductManager p);
void visit(Manager m);
}
// ---------- 2. 具体访问者:薪资计算 ----------
class SalaryVisitor implements Visitor {
public void visit(Engineer e) {
System.out.println(e.getName() + " 薪资:" + e.getBaseSalary() * 1.2);
}
public void visit(ProductManager p) {
System.out.println(p.getName() + " 薪资:" + p.getBaseSalary() * 1.3);
}
public void visit(Manager m) {
System.out.println(m.getName() + " 薪资:" + m.getBaseSalary() * 1.5);
}
}
// ---------- 3. 具体访问者:成本分析 ----------
class CostVisitor implements Visitor {
public void visit(Engineer e) {
System.out.println(e.getName() + " 成本:" + e.getBaseSalary() * 1.8);
}
public void visit(ProductManager p) {
System.out.println(p.getName() + " 成本:" + p.getBaseSalary() * 2.0);
}
public void visit(Manager m) {
System.out.println(m.getName() + " 成本:" + m.getBaseSalary() * 2.5);
}
}
// ---------- 4. 元素接口 ----------
abstract class Employee {
private final String name;
private final double baseSalary;
Employee(String name, double baseSalary) {
this.name = name;
this.baseSalary = baseSalary;
}
public abstract void accept(Visitor visitor);
public String getName() { return name; }
public double getBaseSalary() { return baseSalary; }
}
// ---------- 5. 具体元素 ----------
class Engineer extends Employee {
Engineer(String name, double base) { super(name, base); }
@Override public void accept(Visitor v) { v.visit(this); }
}
class ProductManager extends Employee {
ProductManager(String name, double base) { super(name, base); }
@Override public void accept(Visitor v) { v.visit(this); }
}
class Manager extends Employee {
Manager(String name, double base) { super(name, base); }
@Override public void accept(Visitor v) { v.visit(this); }
}
// ---------- 6. 对象结构 ----------
class EmployeeStructure {
private final List<Employee> employees = new ArrayList<>();
void add(Employee e) { employees.add(e); }
void accept(Visitor visitor) {
for (Employee e : employees) {
e.accept(visitor);
}
}
}
// ---------- 7. 客户端 ----------
public class VisitorDemo {
public static void main(String[] args) {
EmployeeStructure structure = new EmployeeStructure();
structure.add(new Engineer("张三", 20000));
structure.add(new ProductManager("李四", 25000));
structure.add(new Manager("王五", 30000));
System.out.println("===== 薪资统计 =====");
structure.accept(new SalaryVisitor());
System.out.println("\n===== 成本分析 =====");
structure.accept(new CostVisitor());
}
}
关键点:SalaryVisitor 和 CostVisitor 都没有修改 Engineer、ProductManager、Manager 的任何一行代码。新增“培训预算统计”,只需新增一个 TrainingBudgetVisitor,元素侧纹丝不动。
C++ 实现
#include <iostream>
#include <memory>
#include <string>
#include <vector>
class Engineer;
class ProductManager;
class Manager;
// 1. 访问者接口
class Visitor {
public:
virtual ~Visitor() = default;
virtual void visit(const Engineer& e) = 0;
virtual void visit(const ProductManager& p) = 0;
virtual void visit(const Manager& m) = 0;
};
// 2. 元素接口
class Employee {
public:
Employee(std::string name, double base) : name_(std::move(name)), base_(base) {}
virtual ~Employee() = default;
virtual void accept(Visitor& visitor) const = 0;
const std::string& name() const { return name_; }
double baseSalary() const { return base_; }
private:
std::string name_;
double base_;
};
// 3. 具体元素
class Engineer : public Employee {
public:
using Employee::Employee;
void accept(Visitor& v) const override { v.visit(*this); }
};
class ProductManager : public Employee {
public:
using Employee::Employee;
void accept(Visitor& v) const override { v.visit(*this); }
};
class Manager : public Employee {
public:
using Employee::Employee;
void accept(Visitor& v) const override { v.visit(*this); }
};
// 4. 具体访问者
class SalaryVisitor : public Visitor {
public:
void visit(const Engineer& e) override {
std::cout << e.name() << " 薪资:" << e.baseSalary() * 1.2 << "\n";
}
void visit(const ProductManager& p) override {
std::cout << p.name() << " 薪资:" << p.baseSalary() * 1.3 << "\n";
}
void visit(const Manager& m) override {
std::cout << m.name() << " 薪资:" << m.baseSalary() * 1.5 << "\n";
}
};
class CostVisitor : public Visitor {
public:
void visit(const Engineer& e) override {
std::cout << e.name() << " 成本:" << e.baseSalary() * 1.8 << "\n";
}
void visit(const ProductManager& p) override {
std::cout << p.name() << " 成本:" << p.baseSalary() * 2.0 << "\n";
}
void visit(const Manager& m) override {
std::cout << m.name() << " 成本:" << m.baseSalary() * 2.5 << "\n";
}
};
// 5. 对象结构
class EmployeeStructure {
public:
void add(std::unique_ptr<Employee> e) { employees_.push_back(std::move(e)); }
void accept(Visitor& visitor) const {
for (const auto& e : employees_) {
e->accept(visitor);
}
}
private:
std::vector<std::unique_ptr<Employee>> employees_;
};
int main() {
EmployeeStructure s;
s.add(std::make_unique<Engineer>("张三", 20000));
s.add(std::make_unique<ProductManager>("李四", 25000));
s.add(std::make_unique<Manager>("王五", 30000));
std::cout << "===== 薪资统计 =====\n";
SalaryVisitor sv;
s.accept(sv);
std::cout << "\n===== 成本分析 =====\n";
CostVisitor cv;
s.accept(cv);
}
C++ 版与 Java 版结构完全对应。accept 中的 v.visit(*this) 两次分派同样成立:第一次 accept 的虚分派根据元素动态类型选择,第二次 visit 的重载根据 *this 的静态类型(已在具体类中)和 Visitor 的动态类型共同选中。
访问者模式与策略模式、迭代器模式的对比
访问者模式容易被和策略模式混淆(都是“行为型”且都涉及“算法封装”),也容易被和迭代器模式混淆(都涉及“遍历一组对象”)。核心区别如下。
访问者模式 vs 策略模式
| 维度 | 访问者模式 | 策略模式 |
|---|---|---|
| 解决的问题 | 对一组不同类型的元素施加不同操作 | 对同一件事选择不同算法 |
| 算法与谁交互 | 算法作用于元素,需要知道元素的具体类型 | 算法被上下文调用,通常不关心调用者内部结构 |
| 分派方式 | 双重分派(元素类型 + 访问者类型) | 单分派(通常只依赖策略对象的运行时类型) |
| 扩展方向 | 扩展操作容易,扩展元素类型困难 | 扩展算法容易,元素/上下文通常不变 |
| 典型场景 | 编译器 AST 上的类型检查、代码生成、格式化 | 支付方式、折扣计算、排序策略 |
最根本的区别:策略模式封装的是“一个行为族,供上下文替换”;访问者模式封装的是“一组操作,横切一组元素类型”。策略模式的算法之间互不知道;访问者模式的一个访问者必须知道所有元素类型,才能为每个类型提供重载。
访问者模式 vs 迭代器模式
| 维度 | 访问者模式 | 迭代器模式 |
|---|---|---|
| 侧重点 | 对元素做什么(处理) | 如何遍历元素(遍历) |
| 扩展开放的方向 | 操作可扩展 | 遍历方式/集合种类可扩展 |
| 是否依赖具体元素类型 | 依赖,visit 必须按具体类型重载 | 通常不依赖,只依赖统一接口 |
| 典型组合 | 对象结构遍历 + 每元素差异化操作 | 隐藏集合内部表示,统一遍历协议 |
它们经常配合使用:对象结构用迭代器遍历,遍历到的每个元素再 accept 访问者。访问者负责“差异化处理”,迭代器负责“怎么走到每个元素”。
一句话区分:迭代器解决“怎么走过去”,访问者解决“走过去之后对不同的人做什么”。
访问者模式的优缺点
优点
1. 新增操作符合开闭原则。 新增一种作用于元素结构上的操作,只需新增一个 Visitor 实现类,元素类和对象结构无需修改。
2. 相关行为集中,符合单一职责。 同一类操作(例如所有“成本计算”逻辑)集中在一个访问者类中,而不是散落在各个元素类里。元素类保持轻量。
3. 元素结构与操作彻底解耦。 元素只关心“接受访问”,不关心“访问者要做什么”。操作可以独立演化,甚至可以在不重新编译元素类的情况下新增。
4. 跨元素类型的操作成为可能。 某些操作需要同时理解多种元素类型(例如“统计工程师与管理者的人数比例”),访问者天然可以在自己的多个 visit 方法之间共享状态,完成跨类型的聚合逻辑。
5. 用多态替代 instanceof/反射。 双重分派在编译期就确定了 visit 重载的候选集,把“运行时类型检查”的负担从手写 if (e instanceof …) 转移到了类型系统。新增元素类型时,如果某个 Visitor 没有实现对应的 visit,编译器会直接报错,而不是在运行时悄悄走错分支。
缺点
1. 新增元素类型极其困难。 这是访问者模式最根本的代价。每新增一个 ConcreteElement,你必须在 Visitor 接口中新增一个 visit(NewElement) 方法,进而所有已有的 ConcreteVisitor 实现类都必须修改。这意味着:访问者模式对“操作扩展”开放,但对“元素类型扩展”是关闭的,甚至比不用模式更封闭。
2. 可能破坏元素封装。 访问者要完成有意义的操作,通常需要通过元素的 getter 读取内部数据。如果为了“方便访问者”而把本应私有的字段公开,就削弱了封装。好的实践是:元素通过必要的、受控的公开接口暴露访问者所需的最小数据,而不是把内部状态全盘托出。
3. 违背依赖倒置原则。 Visitor 接口依赖的是具体元素类(visit(Engineer) 而非 visit(Employee)),而不是抽象元素。这是双重分派能够工作的前提,但确实让访问者接口与具体元素类型绑定了。
4. 元素类需要一个“为扩展预留”的 accept 方法。 即使某个元素类根本没有访问者需要访问,它也通常要实现 accept,带来轻微的“模式痕迹”。
5. 不适合元素类型频繁变化的场景。 如果元素类型本身还在快速演化,引入访问者模式会让每次类型变更都变成“全量 Visitor 修改”,维护成本远高于收益。这正是“一个反面例子”要强调的。
一个反面例子
访问者模式有一个极窄但极深的适用窗口:元素类型稳定,操作种类多且持续增长。一旦放错场景,代价立竿见影。
// 场景:系统只有 Engineer 和 Manager 两种员工,
// 未来一年只有“算工资”一种操作,且元素类型大概率还会增加(比如新增 Intern)。
// 此时引入 Visitor:
// - Visitor 接口 + 2 个具体 Visitor + 抽象 Employee + 2 个元素 + ObjectStructure
// - 文件数从 2 变 7;
// - 新增 Intern 时,要改 Visitor 接口 + 所有具体 Visitor + 新增元素类;
// - 而如果一开始只用 if-else 或简单多态,新增 Intern 只需加一个类/分支。
判断标准:
- 元素类型已稳定(未来大概率不新增),且操作种类多且会增长 → 访问者模式值得;
- 元素类型仍在频繁变化 → 不要用访问者模式,优先用多态、策略或简单的条件分支;
- 元素类型稳定但操作只有 1~2 种且几乎不变 → 也没必要,直接写在元素类里或普通多态即可;
- 操作会增长,但元素类型也同步增长 → 访问者模式的两难会同时爆发,应慎重评估,或考虑下面提到的 Acyclic Visitor 等变体。
访问者模式不是“消除 if-else”的银弹。它是在用“元素扩展的困难”换取“操作扩展的容易”。 这笔交易是否划算,完全取决于你的变化方向。
应用场景
判断是否适用访问者模式,先问两个问题:
① 对象结构中的元素类型是否足够稳定?
② 是否有多种不相关的操作需要施加于这些元素,且操作种类预计会增长?
两个答案都为“是”,访问者模式才值得考虑。
典型应用
编译器与静态分析。 抽象语法树(AST)是最经典的访问者模式场景。AST 节点类型(变量声明、二元表达式、函数调用、if 语句……)相对稳定,但施加于 AST 上的操作不断增长:类型检查、常量折叠、代码生成、格式化、lint 规则、重构。每新增一种分析或变换,就是一个新的 Visitor,AST 节点类保持稳定。
文档导出与格式转换。 文档对象模型(段落、标题、列表、表格、图片等)元素类型稳定,但导出格式可以是 PDF、HTML、Markdown、纯文本、XML。每种导出格式是一个 Visitor,文档结构不变。
报表与统计系统。 如本文的员工例子:员工类型(工程师、产品经理、管理者)稳定,但薪资、绩效、成本、人效、预算等统计口径不断新增。每种统计是一个 Visitor,员工类不受污染。
UI 组件树的横切操作。 控件树(按钮、输入框、下拉框、表格)结构稳定,但需要施加的操作多样:渲染、主题应用、无障碍属性收集、事件绑定、序列化。访问者可以把每种横切操作从控件类中分离出去。
文件系统遍历中的差异化处理。 文件、目录、符号链接等节点类型稳定,但操作可以是:计算总大小、查找重复文件、生成目录树、权限审计、备份。每种操作是一个 Visitor。
游戏中的实体效果施加。 游戏实体类型(玩家、敌人、道具、障碍物)相对稳定,但“效果”可以多种多样:碰撞处理、伤害计算、渲染、音效触发、AI 感知。访问者可以让每种“效果系统”独立演化。
注意事项
必读:适用性的前提
1. 元素类型必须足够稳定。 访问者模式的全部收益建立在“元素类型很少变化”这一前提上。如果元素类型还在频繁增删,访问者接口的每次变更都会引发所有访问者实现类的连锁修改,维护成本会迅速超过收益。
2. 操作种类多且会增长,才是访问者的用武之地。 只有一两种固定操作时,普通多态或策略模式更直接。访问者模式的价值在于隔离“操作”这一变化维度,没有这个变化维度,模式就是空转。
3. 不要用访问者模式替代所有多态。 如果操作天然属于元素自身(例如“计算自身面积”之于图形),把它放在元素类里仍然是正确的。访问者模式适合的是那些横切多个元素类型、且与元素核心职责关系较弱的操作。
必读:封装与数据暴露
4. 只暴露访问者所需的最小数据,不要为了访问者把元素“扒光”。 访问者需要数据才能工作,但元素不应该因此变成透明的数据结构。通过有意义的公开方法(如 getName()、getBaseSalary())暴露必要信息,而不是把所有字段改成 public。封装一旦被访问者破坏,后续任何元素内部实现的变更都会变得危险。
5. 访问者不应反向修改元素的核心状态。 如果访问者需要回传结果,应通过 visit 方法的返回值或结果收集对象来表达,而不是让访问者直接调用元素的 setter 改状态。访问者模式是“操作外置”,不是“状态控制权外置”。
必读:元素扩展的应对
6. 新增元素类型时,做好“全量 Visitor 修改”的心理准备和工程准备。 这是访问者模式的硬约束,不是可以通过“设计得更好”来消除的。如果项目预期元素类型会增长,应在架构决策阶段就明确这一代价,或者提前评估 Acyclic Visitor 等变体。
7. Acyclic Visitor 可以解耦“新增元素”与“修改所有访问者”的绑定,但它把编译期错误转化成了运行时的类型转换失败。 在 Acyclic Visitor 中,Visitor 接口不再为每种元素声明 visit,而是由具体访问者按需实现特定元素的访问接口,通过运行时转换来分派。代价是:如果你漏掉了某个元素的处理,编译器不会告诉你,只会在运行时失败。是否采用,取决于项目对“编译期安全”和“扩展灵活性”的权衡。
进阶:状态、并发与生命周期
8. 访问者可以是有状态的,但共享有状态访问者需要极其谨慎。 如果访问者在 visit 方法之间累积统计结果(例如求和、计数),它天然是有状态的。有状态访问者不能被多个线程或多次遍历并发共享,除非做同步或每次遍历创建新实例。无状态访问者(纯函数式逻辑)可以安全复用。
9. 对象结构的遍历与访问者的执行通常应放在同一条控制流中,避免访问者在遍历中途被跨线程复用。 如果对象结构很大且遍历耗时,并发化的收益需要与“有状态访问者的线程安全成本”一起评估,不能默认访问者模式“天然适合并行”。
10. 访问者的实例化策略要明确。 无状态访问者可以缓存或做成单例;有状态访问者通常每次遍历新建。如果通过 DI 容器管理访问者,要注意作用域(singleton vs prototype)与状态语义的匹配。
进阶:测试与可维护性
11. 每个具体访问者应独立测试。 用 mock 元素或简单测试桩验证每个 visit 方法的行为。元素侧的 accept 也应测试“是否把正确的访问者分发到了正确的 visit 重载”——这本质上是双重分派链路的集成测试。
12. 访问者的命名应表达“操作语义”,而不是“访问者类型”。 用 SalaryCalculatorVisitor、CostAnalyzerVisitor、TypeCheckVisitor,而不是 VisitorA、MyVisitor。访问者的类名就是它所代表的那项操作的文档。
13. 在 Visitor 接口上写清楚每个 visit 的契约。 包括:该方法是否允许修改元素、是否可能抛出异常、是否有顺序依赖、是否线程安全。访问者是横切多个类型的,契约不清会放大维护成本。
访问者模式不负责什么
| 职责 | 谁负责 | 不该由谁负责 |
|---|---|---|
| 遍历对象结构 | ObjectStructure 或迭代器 | Visitor(访问者只处理单个元素,不负责“怎么走到下一个”) |
| 决定“是否要施加某个操作” | 客户端或对象结构 | Visitor(访问者被调用就执行,不做前置的业务条件判断) |
| 元素的创建与生命周期 | 工厂、DI 容器、业务层 | Visitor(访问者消费元素,不创建元素) |
| 元素类型的演化管理 | 架构设计阶段 | Visitor(模式本身不解决“元素变了怎么办”,只暴露这一代价) |
| 跨元素的并发控制 | 对象结构或外部并发层 | Visitor(访问者不应承担同步职责) |
一句话:访问者模式只做一件事——把“对一组稳定元素施加的可变操作”从元素类中分离出来,并用双重分派把操作精确路由到“访问者 × 元素类型”的组合上。 遍历、创建、并发、元素演化,都不归它管。
总结
- 元素稳定,操作多且会变,才用访问者。 反过来(元素常变,操作很少),访问者模式会让你付出远大于收益的修改成本。
- 双重分派是它的灵魂:
accept决定“谁来接待”,visit(this)决定“来访者做什么”。没有双重分派,访问者模式就退化成了instanceof的包装。 - 它用“元素扩展的封闭”换取“操作扩展的开放”。这是一笔方向性极强的交易,签之前先看清自己的变化方向在哪一侧。
- 落地三件事:元素只暴露最小必要数据,访问者尽量无状态或明确状态作用域,新增元素类型的代价在架构阶段就当作已知成本来管理。
1694



被折叠的 条评论
为什么被折叠?



