访问者模式(Visitor):当数据结构遇到行为变更
访问者模式,说实话,是我早期学习设计模式时觉得最绕的一个。它不像单例、工厂那样直观,甚至第一次看代码时我有点懵——为什么要搞这么复杂?直到我在一个大型报表系统中遇到了真正的痛点,才明白这个模式的精妙之处。
今天我们就来拆解它。我会从结构讲起,再到核心机制——双分派,最后看看Spring框架中是怎么用的。
访问者模式的结构
先看一个经典场景:你有一套对象结构,比如各种类型的员工(经理、程序员、设计师)。现在你想给这些对象增加一些操作,比如计算薪资、生成报表、导出数据。你当然可以在每个类里加方法,但这样每加一个新操作,就得改所有类。
访问者模式的做法是:把操作从对象结构中抽离出来。操作变成“访问者”,对象结构变成“被访问者”。
核心角色就四个:
- Visitor(访问者接口):定义对每种元素类型的访问方法,比如
visit(Manager)、visit(Programmer) - ConcreteVisitor(具体访问者):实现具体的操作逻辑,比如
SalaryCalculator、ReportGenerator - Element(元素接口):定义一个
accept(Visitor)方法,接收访问者 - ConcreteElement(具体元素):实现
accept方法,调用访问者对应的visit方法
我画了一张结构图,帮你理清关系:
你看,访问者和元素之间通过 accept 方法建立联系。元素把自己的类型通过 this 传给访问者,访问者就知道该执行哪个 visit 方法了。
双分派机制:为什么叫“双”分派?
这是访问者模式最核心、也最容易让人困惑的地方。我当年理解它花了不少时间。
先说说普通的方法调用。在Java、C#这类语言里,方法调用是“单分派”的:运行时只根据调用者的实际类型来分派,参数类型在编译时就定死了。
举个例子:
// 单分派的问题
class Animal { }
class Dog extends Animal { }
void feed(Animal a) { System.out.println("喂动物"); }
void feed(Dog d) { System.out.println("喂狗"); }
Animal a = new Dog();
feed(a); // 输出:喂动物(不是喂狗!)
看到了吗?虽然 a 实际是 Dog,但编译器只看声明类型 Animal,所以调用了 feed(Animal)。这就是单分派的局限。
访问者模式用了一个技巧:两次分派。
- 第一次分派:调用
element.accept(visitor)。根据element的实际类型,找到对应的accept方法。 - 第二次分派:在
accept方法内部,调用visitor.visit(this)。这里的this是具体元素类型,所以visit方法能精确匹配。
我画个流程图,你一看就明白:
核心理解:双分派就是让方法调用同时依赖于“访问者的类型”和“元素的类型”。没有访问者模式,你做不到这一点。
代码示例:员工薪资计算
光说不练假把式。我们写个实际例子。
// 元素接口
public interface Employee {
void accept(Visitor visitor);
}
// 具体元素:经理
public class Manager implements Employee {
private double baseSalary = 30000;
private double bonus = 10000;
public void accept(Visitor visitor) {
visitor.visit(this); // 关键:this 是 Manager 类型
}
public double getBaseSalary() { return baseSalary; }
public double getBonus() { return bonus; }
}
// 具体元素:程序员
public class Programmer implements Employee {
private double hourlyRate = 200;
private int hoursWorked = 160;
public void accept(Visitor visitor) {
visitor.visit(this); // 关键:this 是 Programmer 类型
}
public double getHourlyRate() { return hourlyRate; }
public int getHoursWorked() { return hoursWorked; }
}
// 访问者接口
public interface Visitor {
void visit(Manager manager);
void visit(Programmer programmer);
}
// 具体访问者:薪资计算器
public class SalaryCalculator implements Visitor {
public void visit(Manager manager) {
double total = manager.getBaseSalary() + manager.getBonus();
System.out.println("经理薪资:" + total);
}
public void visit(Programmer programmer) {
double total = programmer.getHourlyRate() * programmer.getHoursWorked();
System.out.println("程序员薪资:" + total);
}
}
// 使用
public class Main {
public static void main(String[] args) {
Employee[] employees = {new Manager(), new Programmer()};
Visitor calculator = new SalaryCalculator();
for (Employee e : employees) {
e.accept(calculator); // 双分派发生在这里
}
}
}
运行结果:
经理薪资:40000.0
程序员薪资:32000.0
我的经验:如果你在写 if (employee instanceof Manager) 这样的代码,说明你正在手动模拟双分派。访问者模式就是帮你干掉这些 if-else 的。
访问者模式在 Spring 中的应用:BeanDefinitionVisitor
Spring 框架里有一个 BeanDefinitionVisitor 类,它就是访问者模式的典型应用。不过说实话,这个类在 Spring 3.x 之后用得少了,但它的设计思路非常值得学习。
它的作用是:遍历 BeanDefinition 的属性,并对属性值进行替换或处理。比如,把占位符 ${...} 替换成实际配置值。
核心代码大致是这样的:
// Spring 中的简化版本
public class BeanDefinitionVisitor {
// 访问 BeanDefinition
public void visitBeanDefinition(BeanDefinition beanDefinition) {
// 处理构造参数
visitConstructorArguments(beanDefinition.getConstructorArgumentValues());
// 处理属性值
visitPropertyValues(beanDefinition.getPropertyValues());
}
// 访问构造参数
private void visitConstructorArguments(ConstructorArgumentValues args) {
for (ConstructorArgumentValues.ValueHolder holder : args.getIndexedArgumentValues().values()) {
Object value = holder.getValue();
if (value instanceof TypedStringValue) {
// 处理字符串值,替换占位符
String resolved = resolveStringValue(((TypedStringValue) value).getValue());
((TypedStringValue) value).setValue(resolved);
} else if (value instanceof RuntimeBeanReference) {
// 处理 Bean 引用
String resolved = resolveStringValue(((RuntimeBeanReference) value).getBeanName());
((RuntimeBeanReference) value).setBeanName(resolved);
}
}
}
// 访问属性值(类似逻辑)
private void visitPropertyValues(MutablePropertyValues pvs) {
// ... 遍历并处理每个属性值
}
// 占位符解析(子类可覆盖)
protected String resolveStringValue(String strVal) {
// 默认实现:直接返回
return strVal;
}
}
你看,BeanDefinitionVisitor 就像一个访问者,它“访问”了 BeanDefinition 内部的各个属性节点。每个节点(构造参数、属性值)都接受访问者的处理。
我曾经踩过的坑:有一次我自定义了一个 BeanDefinitionVisitor 子类,想对某些特殊属性做额外处理。结果忘了调用父类的 visitBeanDefinition 方法,导致占位符没被解析,Bean 创建时直接报错。记住:覆盖访问者方法时,一定要调用 super 方法,除非你确定要完全替换逻辑。
什么时候用访问者模式?
我总结了几条经验:
| 适用场景 | 不适用场景 |
|---|---|
| 对象结构稳定,但操作经常变化 | 对象结构经常变化(新增元素类型) |
| 需要对对象结构做多种不相关的操作 | 操作很少,且不会扩展 |
| 想避免操作代码污染元素类 | 元素类很少,且操作简单 |
| 元素类型数量有限且已知 | 元素类型数量不确定或会频繁增加 |
一句话总结:访问者模式把“数据”和“操作”解耦。数据稳定,操作多变时,它就是你的好帮手。但如果你经常要新增元素类型,那还是别用访问者了——每加一个元素,所有访问者都得改,那叫一个酸爽。
好了,关于访问者模式就聊到这儿。记住双分派这个核心机制,下次看到 accept 和 visit 成对出现时,你就知道它们在搞什么名堂了。
公众号:蓝海资料掘金营,微信deep3321