在学习设计模式时,访问者模式经常被认为是比较难理解的一种模式。

原因也很简单:它不像单例、工厂那样直观,而且在日常项目里也不是最高频出现的模式。

不过如果你接触过编译器、AST、复杂数据结构遍历,你很可能已经见过它的影子。

简单来说,访问者模式解决的问题是:

当对象结构稳定,但对对象的操作经常变化时,如何避免频繁修改类结构。

一、先看一个直观例子

假设我们有一个系统,表示不同类型的文件:

  • Word 文档
  • PDF 文件
  • Excel 表格

先定义统一接口:

1
2
3
interface File {
void accept(Visitor visitor);
}

不同文件类型只负责“接待”访问者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class WordFile implements File {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}

class PdfFile implements File {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}

class ExcelFile implements File {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}

这些类本质上是对象结构(数据/类型)的表达。

二、不用访问者会发生什么

假设系统现在要支持这些操作:

  • 打开文件
  • 导出文件
  • 统计文件信息

最直接的写法通常是把行为塞进每个类:

1
2
3
4
5
class WordFile {
void open() {}
void export() {}
void statistics() {}
}

问题很快出现:如果以后要新增一个功能,比如“打印文件”,就必须改动:

  • WordFile
  • PdfFile
  • ExcelFile

每个类都得加 print() 方法。

这显然违背了开闭原则:

对扩展开放,对修改关闭。

三、引入访问者模式

访问者模式的核心思想是:

把“操作”从对象结构中抽离出来。

对象只负责提供结构,操作交给访问者。

先定义访问者接口:

1
2
3
4
5
interface Visitor {
void visit(WordFile file);
void visit(PdfFile file);
void visit(ExcelFile file);
}

实现一个“打开文件”访问者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class OpenVisitor implements Visitor {
@Override
public void visit(WordFile file) {
System.out.println("打开 Word 文件");
}

@Override
public void visit(PdfFile file) {
System.out.println("打开 PDF 文件");
}

@Override
public void visit(ExcelFile file) {
System.out.println("打开 Excel 文件");
}
}

再实现一个“导出文件”访问者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ExportVisitor implements Visitor {
@Override
public void visit(WordFile file) {
System.out.println("导出 Word 文件");
}

@Override
public void visit(PdfFile file) {
System.out.println("导出 PDF 文件");
}

@Override
public void visit(ExcelFile file) {
System.out.println("导出 Excel 文件");
}
}

四、怎么使用

1
2
3
4
File file = new WordFile();
Visitor visitor = new OpenVisitor();

file.accept(visitor);

执行流程是:

1
2
3
file.accept(visitor)

visitor.visit(file)

这个过程本质上就是双重分派(double dispatch)。

五、为什么这样设计

访问者模式最大的优势是:

新增操作非常容易。

比如现在想增加“统计文件大小”,只需要新增一个访问者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class SizeVisitor implements Visitor {
@Override
public void visit(WordFile file) {
System.out.println("统计 Word 文件大小");
}

@Override
public void visit(PdfFile file) {
System.out.println("统计 PDF 文件大小");
}

@Override
public void visit(ExcelFile file) {
System.out.println("统计 Excel 文件大小");
}
}

原来的文件类完全不用动。

六、典型适用场景

访问者模式不是每个项目都会用,但在下面场景非常合适:

1. 编译器 AST

抽象语法树节点(如 ExpressionStatementIfStatement)结构相对稳定,
但操作很多:

  • 代码生成
  • 语义分析
  • 优化

2. 复杂对象结构遍历

例如:

  • 文件系统
  • UI 组件树
  • 文档结构

针对同一结构,可以有不同访问者:

  • 渲染
  • 统计
  • 导出

3. 报表 / 数据分析

同一组数据不断增加新输出方式:

  • 生成 PDF
  • 生成 Excel
  • 生成图表

七、缺点

访问者模式最大缺点是:

一旦对象结构变化,改动成本很高。

例如新增一个文件类型 ImageFile,那所有访问者都要补对应方法:

  • OpenVisitor
  • ExportVisitor
  • SizeVisitor
  • 以及其他所有 Visitor 实现

所以它更适合:

对象结构稳定,但操作经常变化的系统。

总结

访问者模式可以用一句话概括:

对象负责结构,访问者负责行为。

它的核心价值是把操作和数据结构分离。

当系统满足以下条件时,访问者模式通常很合适:

  • 数据结构稳定
  • 操作经常增加
  • 需要对同一组对象执行多种不同操作