一次理解访问者模式
在学习设计模式时,访问者模式经常被认为是比较难理解的一种模式。
原因也很简单:它不像单例、工厂那样直观,而且在日常项目里也不是最高频出现的模式。
不过如果你接触过编译器、AST、复杂数据结构遍历,你很可能已经见过它的影子。
简单来说,访问者模式解决的问题是:
当对象结构稳定,但对对象的操作经常变化时,如何避免频繁修改类结构。
一、先看一个直观例子
假设我们有一个系统,表示不同类型的文件:
- Word 文档
- PDF 文件
- Excel 表格
先定义统一接口:
1 | interface File { |
不同文件类型只负责“接待”访问者:
1 | class WordFile implements File { |
这些类本质上是对象结构(数据/类型)的表达。
二、不用访问者会发生什么
假设系统现在要支持这些操作:
- 打开文件
- 导出文件
- 统计文件信息
最直接的写法通常是把行为塞进每个类:
1 | class WordFile { |
问题很快出现:如果以后要新增一个功能,比如“打印文件”,就必须改动:
WordFilePdfFileExcelFile
每个类都得加 print() 方法。
这显然违背了开闭原则:
对扩展开放,对修改关闭。
三、引入访问者模式
访问者模式的核心思想是:
把“操作”从对象结构中抽离出来。
对象只负责提供结构,操作交给访问者。
先定义访问者接口:
1 | interface Visitor { |
实现一个“打开文件”访问者:
1 | class OpenVisitor implements Visitor { |
再实现一个“导出文件”访问者:
1 | class ExportVisitor implements Visitor { |
四、怎么使用
1 | File file = new WordFile(); |
执行流程是:
1 | file.accept(visitor) |
这个过程本质上就是双重分派(double dispatch)。
五、为什么这样设计
访问者模式最大的优势是:
新增操作非常容易。
比如现在想增加“统计文件大小”,只需要新增一个访问者:
1 | class SizeVisitor implements Visitor { |
原来的文件类完全不用动。
六、典型适用场景
访问者模式不是每个项目都会用,但在下面场景非常合适:
1. 编译器 AST
抽象语法树节点(如 Expression、Statement、IfStatement)结构相对稳定,
但操作很多:
- 代码生成
- 语义分析
- 优化
2. 复杂对象结构遍历
例如:
- 文件系统
- UI 组件树
- 文档结构
针对同一结构,可以有不同访问者:
- 渲染
- 统计
- 导出
3. 报表 / 数据分析
同一组数据不断增加新输出方式:
- 生成 PDF
- 生成 Excel
- 生成图表
七、缺点
访问者模式最大缺点是:
一旦对象结构变化,改动成本很高。
例如新增一个文件类型 ImageFile,那所有访问者都要补对应方法:
OpenVisitorExportVisitorSizeVisitor- 以及其他所有
Visitor实现
所以它更适合:
对象结构稳定,但操作经常变化的系统。
总结
访问者模式可以用一句话概括:
对象负责结构,访问者负责行为。
它的核心价值是把操作和数据结构分离。
当系统满足以下条件时,访问者模式通常很合适:
- 数据结构稳定
- 操作经常增加
- 需要对同一组对象执行多种不同操作
