1. 概述

本文将介绍 GoF 行为型设计模式中的 Visitor 模式

我们会先讲清楚它的设计初衷和解决的问题,再通过 UML 图和代码示例展示其核心实现。最后也会提一下实际使用中的“踩坑点”,帮你权衡是否值得在项目中引入。

2. Visitor 设计模式的核心思想

Visitor 模式的核心目标是:在不修改现有对象结构的前提下,为其添加新的操作行为

设想你有一个复合结构(比如树形结构或文档模型),里面包含多种元素(如 JSON、XML 节点等),这个结构本身是稳定的 —— 你不能改,或者压根不打算频繁增删元素类型。

现在问题来了:如何在不碰原有类的情况下,给这些元素增加新功能?比如打印、序列化、校验等?

Visitor 模式就是为此而生。它的思路很简单粗暴:

✅ 在每个元素类中添加一个 accept(Visitor v) 方法
✅ 所有新操作都封装在不同的 Visitor 实现里
✅ 调用时让结构“接受”某个 Visitor,自动分发到对应处理逻辑

这样一来,算法逻辑就从数据结构中剥离出来了,完美符合 开闭原则(Open/Closed Principle):对扩展开放,对修改关闭。

3. UML 结构图

Visitor-UML

图中展示了两个继承体系:

  • Element 层次:定义可被访问的元素(如 ConcreteElementA, ConcreteElementB
  • Visitor 层次:定义针对不同元素的操作实现

关键流程如下:

  1. 客户端创建一个具体的 Visitor 实例
  2. 将 Visitor 传给根元素(如 Document)的 accept() 方法
  3. 元素内部调用 v.visit(this),触发 双分派(Double Dispatch)
  4. 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


原始标题:Visitor Design Pattern in Java