访问者模式

一个报表系统的扩展噩梦

2019年,我们的报表系统需要支持多种报表类型:

class Report {
    String type; // "SALES", "INVENTORY", "FINANCIAL"

    double total;
    List<Row> rows;
}

void generateReport(Report report) {
    if ("SALES".equals(report.type)) {
        // 生成销售报表
    } else if ("INVENTORY".equals(report.type)) {
        // 生成库存报表
    } else if ("FINANCIAL".equals(report.type)) {
        // 生成财务报表
    }
}

每次加一个新报表类型,都要改 generateReport 方法——违反开闭原则。

访问者模式解决的就是这个问题:把对元素的操作从元素本身分离出来。


二、访问者模式核心🔴

2.1 基本结构

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

// 具体元素
class Document implements Element {
    List<Element> children = new ArrayList<>();

    @Override
    public void accept(Visitor visitor) {
        visitor.visitDocument(this);
        for (Element child : children) {
            child.accept(visitor);
        }
    }
}

class Paragraph implements Element {
    String text;

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

class Image implements Element {
    String url;

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

// 访问者接口
interface Visitor {
    void visitDocument(Document doc);
    void visitParagraph(Paragraph p);
    void visitImage(Image img);
}

// 具体访问者:导出为 HTML
class HtmlExportVisitor implements Visitor {
    private StringBuilder html = new StringBuilder();

    @Override
    public void visitDocument(Document doc) {
        html.append("<html><body>");
    }

    @Override
    public void visitParagraph(Paragraph p) {
        html.append("<p>").append(p.text).append("</p>");
    }

    @Override
    public void visitImage(Image img) {
        html.append("<img src='").append(img.url).append("'/>");
    }

    public String getHtml() {
        html.append("</body></html>");
        return html.toString();
    }
}

// 具体访问者:导出为 Markdown
class MarkdownExportVisitor implements Visitor {
    private StringBuilder md = new StringBuilder();

    @Override
    public void visitDocument(Document doc) {
        // 文档头
    }

    @Override
    public void visitParagraph(Paragraph p) {
        md.append(p.text).append("\n\n");
    }

    @Override
    public void visitImage(Image img) {
        md.append("![image](").append(img.url).append(")\n");
    }

    public String getMarkdown() {
        return md.toString();
    }
}

2.2 双分派机制

// 第一次分派:调用 accept
element.accept(visitor);

// 第二次分派:accept 内部调用 visitor.visitXxx(this)
public void accept(Visitor visitor) {
    visitor.visitDocument(this); // this 是 Document
}

三、编译器中的访问者🟡

3.1 语法树遍历

// 语法树节点
interface ASTNode {
    void accept(Visitor visitor);
}

class AdditionNode implements ASTNode {
    ASTNode left, right;

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

class NumberNode implements ASTNode {
    double value;

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

// 计算器访问者
class EvaluatorVisitor implements Visitor {
    private double result;

    @Override
    public void visitAddition(AdditionNode node) {
        node.left.accept(this);
        double left = result;
        node.right.accept(this);
        double right = result;
        result = left + right;
    }

    @Override
    public void visitNumber(NumberNode node) {
        result = node.value;
    }

    public double getResult() {
        return result;
    }
}

// 打印访问者
class PrinterVisitor implements Visitor {
    private StringBuilder sb = new StringBuilder();

    @Override
    public void visitAddition(AdditionNode node) {
        sb.append("(");
        node.left.accept(this);
        sb.append(" + ");
        node.right.accept(this);
        sb.append(")");
    }

    @Override
    public void visitNumber(NumberNode node) {
        sb.append(node.value);
    }
}

3.2 文件系统统计

// 文件系统节点
interface FileSystemNode {
    long getSize();
    void accept(FileVisitor visitor);
}

class FileNode implements FileSystemNode {
    private String name;
    private long size;

    @Override
    public long getSize() {
        return size;
    }

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

class DirectoryNode implements FileSystemNode {
    private String name;
    private List<FileSystemNode> children = new ArrayList<>();

    @Override
    public long getSize() {
        return children.stream().mapToLong(FileSystemNode::getSize).sum();
    }

    @Override
    public void accept(FileVisitor visitor) {
        visitor.visitDirectory(this);
        for (FileSystemNode child : children) {
            child.accept(visitor);
        }
    }
}

// 文件统计访问者
class FileStatsVisitor implements FileVisitor {
    private long totalSize;
    private int fileCount;
    private int dirCount;

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

    @Override
    public void visitDirectory(DirectoryNode dir) {
        dirCount++;
    }
}

四、访问者模式的优缺点🟡

4.1 优点

优点说明
开闭原则新增操作只需加访问者,不改元素
单一职责相关操作集中在一个访问者类
灵活性可以组合多个访问者

4.2 缺点

缺点说明
违反开闭原则新增元素要改所有访问者
复杂双分派、递归调用,调试困难
耦合元素必须暴露内部结构

【架构权衡】 访问者模式适合元素类型稳定、操作经常变化的场景(如编译器 AST)。如果元素类型也经常变化,访问者模式会导致维护困难。


五、面试总结

级别期望回答
P5能写出访问者模式的基本结构
P6能解释双分派机制
P7能识别访问者模式的适用场景