访问者模式
从编译器 AST 操作说起
编译器把源代码解析成抽象语法树(AST),节点类型有 IfStatement、ForLoop、MethodCall 等,这些类型长期稳定。但要对这棵树执行的操作却频繁变化:语法检查、代码优化、生成字节码……如果把每种操作都加进对应的节点类,每次新增功能就要修改几十个节点类——稳定的结构被多变的操作污染了。
访问者模式将"操作"抽取为独立的 Visitor 类,节点类只提供 accept(Visitor v) 入口。新增功能只需新增一个 Visitor 实现,不碰任何节点类——用"双分派"将操作类型和元素类型解耦。
🔍 定义
访问者模式(Visitor)在不修改已有类的前提下,为对象结构中的元素添加新操作。将操作定义在独立的访问者类中,通过"双分派"机制(accept() + visit())使操作与元素类解耦。
⚠️ 不使用访问者存在的问题
文件系统有 ImageFile 和 TextFile 两种节点,现在需要新增"计算大小"和"压缩文件"两种操作:
| VisitorBadExample.java |
|---|
| package com.example.behavioral.visitor;
/**
* 访问者模式 - 反例
* 问题:新增操作(如压缩)需要修改所有文件类,违反开闭原则
*/
public class VisitorBadExample {
public static void main(String[] args) {
ImageFileBad img = new ImageFileBad("photo.jpg", 1024);
TextFileBad text = new TextFileBad("readme.txt", 256);
System.out.println("总大小: " + (img.getSize() + text.getSize()) + " KB");
img.compress();
text.compress();
// 新增"加密"操作?要在 ImageFileBad 和 TextFileBad 里各自新增方法 ❌
}
}
class ImageFileBad {
private final String name;
private final long size;
public ImageFileBad(String name, long size) { this.name = name; this.size = size; }
public long getSize() { return size; }
// ❌ 操作方法硬编码在文件类里
public void compress() {
System.out.println("[图片压缩] " + name + " 使用 JPEG 压缩");
}
}
class TextFileBad {
private final String name;
private final long size;
public TextFileBad(String name, long size) { this.name = name; this.size = size; }
public long getSize() { return size; }
// ❌ 每种操作都要在所有文件类里重复添加
public void compress() {
System.out.println("[文本压缩] " + name + " 使用 GZIP 压缩");
}
}
|
如果对象结构类型稳定、但操作频繁变化,就该用访问者。
🏗️ 设计模式结构说明
%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#768390'}}}%%
classDiagram
classDef default fill:transparent,stroke:#768390
class FileNode {
<<interface>>
+accept(visitor FileVisitor) void
}
class ImageFile {
-name: String
-size: long
+accept(visitor FileVisitor) void
}
class TextFile {
-name: String
-size: long
+accept(visitor FileVisitor) void
}
class FileVisitor {
<<interface>>
+visit(image ImageFile) void
+visit(text TextFile) void
}
class SizeCalculator {
-totalSize: long
+visit(image ImageFile) void
+visit(text TextFile) void
+getTotalSize() long
}
class Compressor {
+visit(image ImageFile) void
+visit(text TextFile) void
}
FileNode <|.. ImageFile
FileNode <|.. TextFile
FileVisitor <|.. SizeCalculator
FileVisitor <|.. Compressor
note for FileNode "元素接口(Element)"
note for ImageFile "具体元素(ConcreteElement)"
note for FileVisitor "访问者接口(Visitor)"
note for SizeCalculator "具体访问者(ConcreteVisitor)"
💻 设计模式举例说明
| VisitorExample.java |
|---|
| package com.example.behavioral.visitor;
import java.util.List;
/**
* 访问者模式 - 正例
* 新增操作只需新建访问者类,文件类不需要改动
*/
public class VisitorExample {
public static void main(String[] args) {
List<FileNode> files = List.of(
new ImageFile("photo.jpg", 1024),
new TextFile("readme.txt", 256),
new ImageFile("banner.png", 512)
);
// ✅ 用不同访问者执行不同操作,文件类不变
FileVisitor sizeCalc = new SizeCalculator();
FileVisitor compressor = new Compressor();
files.forEach(f -> f.accept(sizeCalc));
System.out.println("总大小: " + ((SizeCalculator) sizeCalc).getTotalSize() + " KB");
System.out.println("--- 压缩 ---");
files.forEach(f -> f.accept(compressor));
// ✅ 新增"加密"操作:只需 class EncryptVisitor implements FileVisitor {...}
}
}
// 节点接口:接受访问者
interface FileNode {
void accept(FileVisitor visitor);
long getSize();
}
// 具体节点:图片文件
class ImageFile implements FileNode {
private final String name;
private final long size;
public ImageFile(String name, long size) { this.name = name; this.size = size; }
@Override public void accept(FileVisitor visitor) { visitor.visitImage(this); }
@Override public long getSize() { return size; }
public String getName() { return name; }
}
// 具体节点:文本文件
class TextFile implements FileNode {
private final String name;
private final long size;
public TextFile(String name, long size) { this.name = name; this.size = size; }
@Override public void accept(FileVisitor visitor) { visitor.visitText(this); }
@Override public long getSize() { return size; }
public String getName() { return name; }
}
// 访问者接口
interface FileVisitor {
void visitImage(ImageFile imageFile);
void visitText(TextFile textFile);
}
// ✅ 具体访问者:计算大小
class SizeCalculator implements FileVisitor {
private long totalSize = 0;
@Override public void visitImage(ImageFile f) { totalSize += f.getSize(); }
@Override public void visitText(TextFile f) { totalSize += f.getSize(); }
public long getTotalSize() { return totalSize; }
}
// ✅ 具体访问者:压缩文件(新增操作,不改文件类)
class Compressor implements FileVisitor {
@Override
public void visitImage(ImageFile f) {
System.out.println("[图片压缩] " + f.getName() + " 使用 JPEG 压缩");
}
@Override
public void visitText(TextFile f) {
System.out.println("[文本压缩] " + f.getName() + " 使用 GZIP 压缩");
}
}
|
⚖️ 优缺点
优点:
- 符合**开闭原则**:新增操作只需新增访问者类,元素类不变
- 将相关操作集中在访问者类中,避免分散在各个元素类
缺点:
- 如果元素类型经常变化(需要新增新节点类型),所有访问者都要修改
- 访问者需要访问元素的内部状态,可能需要暴露本应私有的字段
使用前提
访问者模式适合**对象结构稳定(类型不频繁变化)、操作频繁变化**的场景。如果类型本身需要经常新增,该模式反而会带来大量修改。
🔗 与其它模式的关系
相似模式防混淆:
| 模式 |
谁定义操作 |
对象结构可否变化 |
| 访问者(Visitor) |
访问者类(外部) |
稳定(不频繁新增类型) |
| 策略(Strategy) |
策略类(外部) |
不涉及对象结构 |
| 迭代器(Iterator) |
客户端遍历 |
关注顺序访问,不关注操作类型 |
🗂️ 应用场景
- 对象结构稳定,但需要频繁添加新操作(如编译器 AST 处理、报表生成)
- 对象结构中的元素有多种类型,需要针对每种类型实现不同的操作逻辑
- Java 编译器的 AST 遍历、XML 处理框架
🏭 工业视角
双分派:Java 用两次多态模拟"类型 × 操作"矩阵
大多数面向对象语言(包括 Java)只支持**单分派(Single Dispatch)——方法调用时只根据**接收者的运行时类型**决定执行哪个版本。但访问者模式需要同时根据**元素类型**和**访问者类型**两个维度来决定行为,这就是**双分派(Double Dispatch)。
Java 无法直接支持双分派,访问者模式用两次多态调用来模拟:
| 双分派的两步调用 |
|---|
| // 第一次分派:根据 element 的运行时类型,调用对应的 accept()
element.accept(visitor); // → ImageFile.accept() 或 TextFile.accept()
// 第二次分派:在 accept() 内部,根据 visitor 的编译时类型,调用对应的 visit()
visitor.visit(this); // → SizeCalculator.visit(ImageFile) 或 Compressor.visit(ImageFile)
|
两次动态绑定合在一起,实现了"操作类型 × 元素类型"的矩阵式分发。
记忆要点
element.accept(visitor) 决定"哪种元素",visitor.visit(concreteElement) 决定"哪种操作"。
拆开看都是普通单分派,合在一起就实现了双分派——这是访问者模式最精妙也最难读懂的地方。
实用场景稀少,但编译器是经典落地
访问者模式在工业界使用频率很低,原因是它对"元素类型稳定"的要求极为苛刻——每新增一种节点类型,所有访问者类都必须修改。只有在以下场景才真正值得使用:
- 编译器 / 解释器的 AST 遍历:语法节点类型固定,但需要频繁新增分析 Pass(类型检查、代码优化、代码生成……)
- 文档树的多种处理:节点类型稳定,处理逻辑多变
实际开发慎用
《设计模式之美》明确建议:在没有特别必要的情况下,不要使用访问者模式。
双分派机制让代码可读性和可维护性明显变差,结构复杂度远高于收益。
如果只是想对已有类新增操作,优先考虑直接扩展或函数式方案。