访问者模式:让你的代码优雅地"拜访"对象结构
当你想为一组稳定的对象结构添加千变万化的操作,又不想频繁修改这些对象时,访问者模式就是你的不二之选。
场景
假如你正在开发一款硬件检测软件。软件需要识别电脑中的各类硬件(CPU、GPU、硬盘、内存),并对它们执行多种操作:读取型号、检测温度、跑分评测、健康诊断......
最直观的想法是:在每个硬件类中直接添加这些操作方法。但这样会带来两个严重问题:
- 类爆炸 :每增加一种操作,就要修改所有硬件类,违反开闭原则。
- 重复代码:遍历组合结构(比如一台电脑包含多个硬件)的逻辑会在每个操作中重复。
访问者模式正是为解决这类问题而生------它将数据结构 与作用于结构上的操作分离,让你可以在不修改元素类的前提下定义新的操作。
访问者模式精讲
定义
表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。
------ GoF《设计模式》
核心角色
| 角色 | 职责 | 示例中的类 |
|---|---|---|
| Visitor(抽象访问者) | 为每个具体元素声明访问方法 | Visitor 接口 |
| ConcreteVisitor(具体访问者) | 实现每种操作的具体逻辑 | InfoVisitor, TempVisitor 等 |
| Element(抽象元素) | 定义接受访问者的入口 | Hardware 接口 |
| ConcreteElement(具体元素) | 实现 accept 方法,调用访问者对应方法 |
CPU, GPU, Disk, Memory |
| ObjectStructure(对象结构) | 容纳元素的集合,提供遍历能力 | Computer 类 |
代码逐层拆解
我们以上述硬件检测代码为例,一步步剖析访问者模式的实现。
1. 抽象元素与具体元素
java
java
interface Hardware {
void accept(Visitor visitor);
}
class CPU implements Hardware {
@Override
public void accept(Visitor visitor) {
visitor.visit(this); // 关键:回调访问者
}
}
// GPU、Disk、Memory 类似......
每个硬件都知道自己是什么类型,在 accept 方法中调用访问者的 visit(this)。由于 Java 支持重载,visitor.visit(this) 会精确匹配到 visit(CPU cpu) 方法------双分派的魅力就在于此。
2. 抽象访问者
java
csharp
interface Visitor {
void visit(CPU cpu);
void visit(GPU gpu);
void visit(Disk disk);
void visit(Memory memory);
}
每个具体操作(信息读取、温度检测等)都需要实现这四个方法。这样一来,新增操作只需新增一个 Visitor 实现类,无需触碰任何硬件类。
3. 对象结构:组合模式加持的电脑
java
typescript
class Computer implements Hardware {
List<Hardware> parts = Arrays.asList(new CPU(), new GPU(), new Disk(), new Memory());
@Override
public void accept(Visitor visitor) {
parts.forEach(part -> part.accept(visitor)); // 遍历整棵树
}
}
Computer 本身也是一个 Hardware(元素),但它内部包含多个子硬件。当访问者访问 Computer 时,它会将访问请求传播到所有子部件------这实现了对复合对象结构的自动遍历。
访问者模式常与组合模式搭配使用,形成强大的树形结构处理能力。
4. 多个具体访问者
- InfoVisitor:输出硬件名称
- TempVisitor:输出模拟温度值
- ScoreVisitor:输出跑分
- HealthVisitor:输出健康状态
每个访问者关注一组完全不同的行为,但它们都复用同一个稳定的硬件类层次结构。
5. 客户端调用
java
java
Hardware computer = new Computer();
computer.accept(new InfoVisitor()); // 第1轮:读取型号
computer.accept(new TempVisitor()); // 第2轮:检测温度
computer.accept(new ScoreVisitor()); // 第3轮:跑分
computer.accept(new HealthVisitor()); // 第4轮:诊断
同一棵对象树,无需修改任何元素类,就能执行四种完全不同的操作------这就是访问者模式的核心价值。
运行结果
text
diff
=== 第1轮:读取硬件型号 ===
CPU GPU Disk Memory
=== 第2轮:检测温度 ===
65℃ 72℃ 42℃ 45℃
=== 第3轮:性能跑分 ===
12万 30万 3万 8万
=== 第4轮:健康诊断 ===
正常 正常 警告 正常
优点与缺点
✅ 优点
- 开闭原则:添加新操作只需增加新访问者,元素类无需改动。
- 集中相关行为:同一操作的相关代码被聚集在一个访问者类中,而不是散落在各个元素类里。
- 方便积累状态:访问者可以在遍历过程中维护自己的状态(例如计算总和、最大值等)。
- 与组合模式珠联璧合:轻松对复杂树形结构执行各种算法。
❌ 缺点
- 增加新元素困难 :每增加一个
ConcreteElement,就要修改所有访问者接口及其实现类,违背开闭原则。因此访问者模式适用于元素类结构相对稳定的场景。 - 破坏封装:访问者为了执行操作,往往需要访问元素内部状态,这可能迫使元素暴露过多细节。
- 依赖具体类型:访问者模式强依赖元素的具体类型(通过重载实现),在动态类型语言中效果会打折扣。
适用场景指南
✅ 适合使用访问者模式的情形
- 对象结构稳定(很少增加新类型的元素),但经常需要在该结构上定义新的操作。
- 需要对不同元素执行差异化的操作,且操作逻辑复杂、容易变化。
- 对象结构中包含组合对象(如树、列表),希望避免在每个操作中重复编写遍历代码。
❌ 不适合使用的情形
- 元素类型频繁变动(比如不断添加新的硬件类型)。
- 元素类的内部细节完全对访问者不可见(或者你不想暴露)。
- 操作数量很少且固定,直接在元素类中添加方法更简单。
实际应用拓展
访问者模式在真实项目中并不罕见,例如:
- 编译器:AST(抽象语法树)的语义检查、代码生成、优化等操作,AST 节点类型有限,但操作极多。
- 报表系统:对不同类型的报表元素(文本、图片、表格)执行导出、预览、打印等操作。
- UI 框架 :事件处理系统,如遍历控件树并分发
onClick、onResize等事件。 - Java NIO 的
FileVisitor:方便你遍历文件树并执行自定义操作。
结语
访问者模式通过双分派机制,巧妙地解决了"稳定的对象结构 + 多变的行为"这一设计难题。它把变化封装在访问者一侧,让系统具有良好的扩展性。当然,任何设计模式都是一把双刃剑:在带来灵活性的同时,也增加了理解的复杂度。当你下一次面对同类需求时,不妨想想------我这个对象结构真的稳定吗?如果是,那就放心地请访问者来帮忙吧!
js
// 抽象元素:硬件(树节点)
interface Hardware {
void accept(Visitor visitor);
}
// 具体元素:CPU
static class CPU implements Hardware {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
// 具体元素:GPU
static class GPU implements Hardware {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
// 具体元素:硬盘
static class Disk implements Hardware {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
// 具体元素:内存
static class Memory implements Hardware {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
// 电脑 = 一棵硬件树
static class Computer implements Hardware {
List<Hardware> parts = Arrays.asList(new CPU(), new GPU(), new Disk(), new Memory());
@Override
public void accept(Visitor visitor) {
parts.forEach(part -> part.accept(visitor)); // 遍历整棵树
}
}
// 抽象访问者
interface Visitor {
void visit(CPU cpu);
void visit(GPU gpu);
void visit(Disk disk);
void visit(Memory memory);
}
// 访问者1:读取信息
static class InfoVisitor implements Visitor {
public void visit(CPU cpu) {
System.out.print("CPU ");
}
public void visit(GPU gpu) {
System.out.print("GPU ");
}
public void visit(Disk d) {
System.out.print("Disk ");
}
public void visit(Memory m) {
System.out.print("Memory ");
}
}
// 访问者2:检测温度
static class TempVisitor implements Visitor {
public void visit(CPU cpu) {
System.out.print("65℃ ");
}
public void visit(GPU gpu) {
System.out.print("72℃ ");
}
public void visit(Disk d) {
System.out.print("42℃ ");
}
public void visit(Memory m) {
System.out.print("45℃ ");
}
}
// 访问者3:性能跑分
static class ScoreVisitor implements Visitor {
public void visit(CPU cpu) {
System.out.print("12万 ");
}
public void visit(GPU gpu) {
System.out.print("30万 ");
}
public void visit(Disk d) {
System.out.print("3万 ");
}
public void visit(Memory m) {
System.out.print("8万 ");
}
}
// 访问者4:健康诊断
static class HealthVisitor implements Visitor {
public void visit(CPU cpu) {
System.out.print("正常 ");
}
public void visit(GPU gpu) {
System.out.print("正常 ");
}
public void visit(Disk d) {
System.out.print("警告 ");
}
public void visit(Memory m) {
System.out.print("正常 ");
}
}
// 主方法:同一棵树遍历4次
public static void main(String[] args) {
Hardware computer = new Computer(); // 同一颗树,不变
System.out.println("=== 第1轮:读取硬件型号 ===");
computer.accept(new InfoVisitor());
System.out.println("\n=== 第2轮:检测温度 ===");
computer.accept(new TempVisitor());
System.out.println("\n=== 第3轮:性能跑分 ===");
computer.accept(new ScoreVisitor());
System.out.println("\n=== 第4轮:健康诊断 ===");
computer.accept(new HealthVisitor());
}