在面向对象设计中,我们经常会遇到一类问题:系统中存在由多个不同类组成的复杂对象结构,例如树形菜单、抽象语法树、公司组织架构等。当我们需要对这些结构中的元素执行多种不同的操作,而又不希望频繁修改元素本身的类时,传统的多态方案会显得力不从心。访问者模式提供了一种将算法与对象结构分离的思路,而它背后的核心机制正是双重分发技术。

什么是访问者模式
访问者模式属于一种行为型设计模式,它允许你在不改变各元素类的前提下定义作用于这些元素的新操作。模式中包含两个关键角色:一个是元素接口,通常声明一个接受访问者的方法;另一个是访问者接口,为每一种具体元素类型定义一个访问方法。通过让元素对象把自己传给访问者,操作逻辑就被集中到了访问者实现中。
这种设计特别适合元素类型相对稳定,但针对元素的操作经常变化的系统。例如一个报表系统,底层数据节点类型固定为公司、部门、员工,但上层可能需要导出XML、计算薪资总额、生成组织架构图等多种功能。如果把这些功能都写进节点类,类会迅速膨胀且难以维护。访问者模式让节点类保持干净,只负责自身数据,而把行为外包。
双重分发是如何工作的
普通的方法调用是单分发的:调用哪个方法由对象运行时类型决定,但方法参数类型在编译期就已绑定。访问者模式需要依据元素的真实类型来调用访问者中对应的方法,仅依靠单分发无法在编译期确定,因此引入了双重分发。第一次分发发生在客户端代码调用访问者的 visit 方法并传入元素时,此时根据访问者类型确定大致方法;第二次分发发生在元素对象的 accept 方法内部,元素把自己作为参数回传给访问者,由于元素真实类型已知,虚拟机就能调用访问者中精确匹配该类型的重载方法。
下面用 Java 代码展示这一机制。我们首先定义元素与访问者接口:
// 元素接口
interface Element {
void accept(Visitor v);
}
// 访问者接口
interface Visitor {
void visit(Employee e);
void visit(Department d);
}
// 具体元素:员工
class Employee implements Element {
public String name;
public int salary;
public Employee(String n, int s) {
name = n;
salary = s;
}
public void accept(Visitor v) {
// 第二次分发:调用精确匹配Employee类型的visit
v.visit(this);
}
}
// 具体元素:部门
class Department implements Element {
public String title;
public Department(String t) {
title = t;
}
public void accept(Visitor v) {
v.visit(this);
}
}
在上述代码中,当客户端调用 emp.accept(visitor) 时,先进入 Employee 的 accept 方法,再由其内部调用 visitor.visit(this)。由于 this 是 Employee 类型,因此会定位到 Visitor 接口里接收 Employee 参数的 visit 方法,这就是双重分发的落地方式。
遍历复杂对象结构
复杂结构通常不是单个对象,而是组合体。我们可以让容器元素在 accept 中遍历其子元素并逐一传递访问者,从而实现整体结构的递归访问。以下示例展示部门包含员工,并在自身 accept 中分发到访问者后继续让员工接受同一访问者:
import java.util.ArrayList;
import java.util.List;
class Department implements Element {
public String title;
public List<Employee> members = new ArrayList<>();
public Department(String t) {
title = t;
}
public void add(Employee e) {
members.add(e);
}
public void accept(Visitor v) {
v.visit(this);
for (Employee e : members) {
e.accept(v);
}
}
}
// 具体访问者:薪资统计
class SalaryCounter implements Visitor {
public int total = 0;
public void visit(Employee e) {
total += e.salary;
}
public void visit(Department d) {
System.out.println("进入部门:" + d.title);
}
}
通过这种结构,客户端只需要拿到顶层部门对象,调用一次 dept.accept(counter),就能借助双重分发自动在员工节点上累加薪资,而无需在节点类里写任何统计代码。复杂结构的遍历逻辑与业务操作彻底解耦。
如果后续要新增导出 JSON 的功能,只需再写一个实现 Visitor 接口的 JsonExporter 类,原元素类一行都不用改。这种扩展性是以增加访问者类数量为代价的,但在操作多变的场景下收益明显。
双重分发的局限与注意事项
双重分发依赖元素类型在编译期可被访问者接口穷举。一旦新增一种元素子类,例如临时工 Intern,所有访问者接口和实现都要补充对应的 visit 方法,否则编译失败。这意味着元素类层次应当相对稳定,否则访问者模式反而会成为负担。
另外,元素在 accept 中把自身暴露给访问者,访问者可以读取甚至修改元素内部状态,这在一定程度上削弱了元素的封装性。实践中可通过仅向访问者提供必要的只读方法来缓解。与反射或注解处理器等方案相比,访问者模式的性能更好,因为分发在编译期就已确定方法签名,没有运行期类型探测开销。
小结
访问者模式通过元素 accept 与访问者 visit 的相互回调,实现了基于真实类型的双重分发,使复杂对象结构上的算法可以独立演化。理解第二次分发发生在元素内部这一关键点,就能明白为何它能突破单分发的限制。在编译器、规则引擎、文档转换等结构中,该技术仍是非常实用的工具。