访问者模式(Visitor)

访问者模式,说白了就是让你在不修改现有类的前提下,给这些类增加新的操作。我刚开始接触这个模式时,觉得它有点绕——为什么要绕这么大一圈?后来在项目中遇到一个实际问题,才真正体会到它的价值。

模式定义

访问者模式表示一个作用于某对象结构中的各元素的操作。它允许你在不改变各元素类的前提下,定义作用于这些元素的新操作。

核心思想其实很简单:把操作从对象结构中分离出来。你想想看,如果有一组稳定的对象结构,但你需要不断给它们添加新功能,直接修改每个类显然不是好办法。访问者模式就是来解决这个问题的。

模式结构

访问者模式涉及以下几个角色:

  • Visitor(访问者):声明一组访问方法,每个方法对应一个具体的元素类型
  • ConcreteVisitor(具体访问者):实现具体的访问逻辑
  • Element(元素):声明一个accept方法,接收访问者作为参数
  • ConcreteElement(具体元素):实现accept方法
  • ObjectStructure(对象结构):管理元素集合,提供遍历方式

关键点:访问者模式使用了"双重分派"机制。第一次分派是调用元素的accept方法,第二次分派是调用访问者的visit方法。这样就把操作和数据结构解耦了。

SVG:访问者模式核心结构图

访问者模式核心结构 <<interface>> Visitor + visitElementA(ElementA) + visitElementB(ElementB) ConcreteVisitor1 + visitElementA(...) + visitElementB(...) ConcreteVisitor2 + visitElementA(...) + visitElementB(...) <<interface>> Element + accept(Visitor) ElementA + accept(Visitor v) v.visitElementA(this) ElementB + accept(Visitor v) v.visitElementB(this) ObjectStructure + attach(Element) + accept(Visitor) 双重分派:element.accept(visitor) → visitor.visitElement(element) 操作与数据结构完全解耦,新增操作只需添加新的 ConcreteVisitor

实际应用场景

我在项目中遇到过这样一个场景:一个文档系统包含多种节点类型——文本节点、图片节点、表格节点。需要对这些节点做各种操作:导出HTML、导出PDF、统计字数、检查链接有效性等等。

如果不用访问者模式,每增加一个操作就要修改所有节点类。用了访问者模式后,每个操作就是一个新的访问者,节点类完全不用动。

适用场景

  • 对象结构稳定,但需要频繁添加新操作
  • 需要对一个对象结构中的不同元素执行不同操作
  • 操作涉及多个不相关的类,不想把这些操作分散到各个类中

代码示例

来看一个文件系统的例子。我们有文件和文件夹两种元素,需要做两种操作:计算大小和打印结构。

// 访问者接口
interface Visitor {
    void visitFile(File file);
    void visitFolder(Folder folder);
}

// 元素接口
interface Element {
    void accept(Visitor visitor);
}

// 文件元素
class File implements Element {
    private String name;
    private int size;

    public File(String name, int size) {
        this.name = name;
        this.size = size;
    }

    public String getName() { return name; }
    public int getSize() { return size; }

    @Override
    public void accept(Visitor visitor) {
        visitor.visitFile(this);
    }
}

// 文件夹元素
class Folder implements Element {
    private String name;
    private List<Element> children = new ArrayList<>();

    public Folder(String name) {
        this.name = name;
    }

    public void add(Element element) {
        children.add(element);
    }

    public String getName() { return name; }
    public List<Element> getChildren() { return children; }

    @Override
    public void accept(Visitor visitor) {
        visitor.visitFolder(this);
    }
}

// 具体访问者:计算大小
class SizeCalculator implements Visitor {
    private int totalSize = 0;

    @Override
    public void visitFile(File file) {
        totalSize += file.getSize();
    }

    @Override
    public void visitFolder(Folder folder) {
        for (Element child : folder.getChildren()) {
            child.accept(this);
        }
    }

    public int getTotalSize() { return totalSize; }
}

// 具体访问者:打印结构
class StructurePrinter implements Visitor {
    private int indent = 0;

    @Override
    public void visitFile(File file) {
        printIndent();
        System.out.println(file.getName() + " (" + file.getSize() + " bytes)");
    }

    @Override
    public void visitFolder(Folder folder) {
        printIndent();
        System.out.println("[文件夹] " + folder.getName());
        indent++;
        for (Element child : folder.getChildren()) {
            child.accept(this);
        }
        indent--;
    }

    private void printIndent() {
        for (int i = 0; i < indent; i++) {
            System.out.print("  ");
        }
    }
}

// 使用示例
public class Main {
    public static void main(String[] args) {
        Folder root = new Folder("root");
        root.add(new File("readme.txt", 100));
        root.add(new File("logo.png", 500));

        Folder src = new Folder("src");
        src.add(new File("main.java", 200));
        src.add(new File("utils.java", 150));
        root.add(src);

        // 计算大小
        SizeCalculator sizeCalc = new SizeCalculator();
        root.accept(sizeCalc);
        System.out.println("总大小: " + sizeCalc.getTotalSize() + " bytes");

        // 打印结构
        StructurePrinter printer = new StructurePrinter();
        root.accept(printer);
    }
}

优缺点分析

优点 缺点
符合开闭原则:新增操作无需修改现有元素类 增加新元素类困难:需要修改所有访问者接口
将相关操作集中到一个访问者中 访问者需要知道元素内部细节,破坏了封装
便于添加新操作 元素数量较多时,访问者接口会变得臃肿

避坑指南:我曾经在一个项目中滥用访问者模式,把本来可以用简单循环解决的问题硬套上了访问者模式。结果代码变得又长又绕。记住:访问者模式适用于对象结构稳定但操作频繁变化的场景。如果元素类型经常变化,就别用这个模式。

与策略模式的对比

很多人会把访问者模式和策略模式搞混。我简单说说区别:

  • 策略模式:针对同一类型对象,封装不同的算法。比如对订单使用不同的折扣策略。
  • 访问者模式:针对不同类型对象,封装不同的操作。比如对文件、文件夹、链接分别做导出操作。

说白了,策略模式是"一个对象,多种算法";访问者模式是"多种对象,多种操作"。

实际项目中的思考

我记得有一次做代码生成器,需要遍历抽象语法树(AST)。AST的节点类型很多——变量声明、函数定义、条件语句等等。每个节点都需要做类型检查、代码生成、优化等操作。

如果用传统的if-else或者switch,代码会变得极其混乱。访问者模式完美解决了这个问题:每个操作就是一个访问者,AST节点完全不用修改。后来要增加一个新的"代码混淆"功能,只需要写一个新的访问者就行,AST的代码一行都没动。

嗯,这里要注意一点:访问者模式虽然强大,但不要为了用而用。如果你的对象结构经常变化,或者操作数量很少,用简单的多态或者策略模式可能更合适。


公众号:蓝海资料掘金营,微信deep3321