1. 概述
本文将介绍 GoF 行为型设计模式中的 Visitor 模式。
我们会先讲清楚它的设计初衷和解决的问题,再通过 UML 图和代码示例展示其核心实现。最后也会提一下实际使用中的“踩坑点”,帮你权衡是否值得在项目中引入。
2. Visitor 设计模式的核心思想
Visitor 模式的核心目标是:在不修改现有对象结构的前提下,为其添加新的操作行为。
设想你有一个复合结构(比如树形结构或文档模型),里面包含多种元素(如 JSON、XML 节点等),这个结构本身是稳定的 —— 你不能改,或者压根不打算频繁增删元素类型。
现在问题来了:如何在不碰原有类的情况下,给这些元素增加新功能?比如打印、序列化、校验等?
Visitor 模式就是为此而生。它的思路很简单粗暴:
✅ 在每个元素类中添加一个 accept(Visitor v)
方法
✅ 所有新操作都封装在不同的 Visitor
实现里
✅ 调用时让结构“接受”某个 Visitor,自动分发到对应处理逻辑
这样一来,算法逻辑就从数据结构中剥离出来了,完美符合 开闭原则(Open/Closed Principle):对扩展开放,对修改关闭。
3. UML 结构图
图中展示了两个继承体系:
- Element 层次:定义可被访问的元素(如
ConcreteElementA
,ConcreteElementB
) - Visitor 层次:定义针对不同元素的操作实现
关键流程如下:
- 客户端创建一个具体的 Visitor 实例
- 将 Visitor 传给根元素(如
Document
)的accept()
方法 - 元素内部调用
v.visit(this)
,触发 双分派(Double Dispatch) - JVM 根据
this
的实际类型,自动调用 Visitor 中对应的方法
⚠️ 注意:Java 本身不支持多态方法重载的运行时分发,所以 visit(this)
这一步是实现类型精准匹配的关键。
4. 代码实现
我们以一个文档处理系统为例,包含 JSON 和 XML 两种元素。
4.1 元素基类与文档结构
public abstract class Element {
public abstract void accept(Visitor v);
}
Document
是一个复合元素,包含多个子元素:
public class Document extends Element {
private final String uuid;
private final List<Element> elements = new ArrayList<>();
public Document(String uuid) {
this.uuid = uuid;
}
public void addElement(Element element) {
elements.add(element);
}
@Override
public void accept(Visitor v) {
for (Element e : elements) {
e.accept(v);
}
}
}
4.2 具体元素实现
每个具体元素只需实现 accept
方法,内容几乎一样,属于样板代码:
public class JsonElement extends Element {
public final String uuid;
public JsonElement(String uuid) {
this.uuid = uuid;
}
@Override
public void accept(Visitor v) {
v.visit(this); // 双分派:把 this 传给 visit 方法
}
}
public class XmlElement extends Element {
public final String uuid;
public XmlElement(String uuid) {
this.uuid = uuid;
}
@Override
public void accept(Visitor v) {
v.visit(this);
}
}
4.3 Visitor 接口与实现
Visitor 接口定义了针对每种元素的处理方法:
public interface Visitor {
void visit(JsonElement je);
void visit(XmlElement xe);
}
具体实现类可以根据业务自由发挥:
public class ElementVisitor implements Visitor {
@Override
public void visit(XmlElement xe) {
System.out.println(
"processing an XML element with uuid: " + xe.uuid);
}
@Override
public void visit(JsonElement je) {
System.out.println(
"processing a JSON element with uuid: " + je.uuid);
}
}
每个 visit
方法都能直接访问对应类型的对象,无需强制类型转换,类型安全拉满。
5. 测试验证
来跑个 demo 看看效果:
public class VisitorDemo {
public static void main(String[] args) {
Visitor v = new ElementVisitor();
Document d = new Document("doc-123");
d.addElement(new JsonElement("fdbc75d0-5067-49df-9567-239f38f01b04"));
d.addElement(new JsonElement("81e6c856-ddaf-43d5-aec5-8ef977d3745e"));
d.addElement(new XmlElement("091bfcb8-2c68-491a-9308-4ada2687e203"));
d.accept(v);
}
private static String generateUuid() {
return java.util.UUID.randomUUID().toString();
}
}
输出结果:
processing a JSON element with uuid: fdbc75d0-5067-49df-9567-239f38f01b04
processing a JSON element with uuid: 81e6c856-ddaf-43d5-aec5-8ef977d3745e
processing an XML element with uuid: 091bfcb8-2c68-491a-9308-4ada2687e203
可以看到,每个元素都被正确“访问”,并路由到了对应的 visit
方法,拿到了各自的 uuid
数据。
6. 缺点与注意事项
Visitor 模式虽然强大,但也不是银弹,使用时要注意以下几点:
❌ 新增元素类型成本高
一旦你要加 YamlElement
,所有已有的 Visitor 实现都得补上 visit(YamlElement)
方法。如果有十几个 Visitor,改起来很痛苦。
❌ 逻辑分散,不利于维护
同一个业务逻辑(比如“导出”)被拆到多个 Visitor 中,阅读代码时需要跳来跳去,心智负担大。
❌ 违反封装原则
Visitor 能直接访问元素内部字段(如 uuid
),相当于把私有数据暴露了出去,破坏了封装性。
✅ 适合场景总结:
- 对象结构稳定,很少新增元素类型
- 需要频繁添加新操作(如分析、打印、转换)
- 操作逻辑复杂,且依赖具体类型
7. 总结
Visitor 模式是一种典型的 行为与数据分离 的设计方式。它通过双分派机制,让外部逻辑可以“注入”到对象结构中,而不污染原有类。
虽然新增元素会带来维护成本,但在结构稳定、操作多变的场景下(比如编译器 AST 处理、文档解析),它依然是简单粗暴又高效的解决方案。
💡 小贴士:JDK 中也有 Visitor 的影子,比如 java.nio.file.FileVisitor
就是文件遍历的经典应用。
完整代码已托管至 GitHub:https://github.com/example-java/design-patterns-behavioral