Skip to content

访问者模式 (Visitor)

一、定义

一句话概括:表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。

官方定义:Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.

二、解决的问题

2.1 问题场景

在开发中,当需要对一组不同类型的对象执行不同的操作时,例如:

  • 编译器:对 AST(抽象语法树)的不同节点进行类型检查、代码生成、格式化
  • 电商系统:对不同类型的商品(电子产品、图书、食品)计算税费、生成报表
  • 文档处理:对不同类型的文档元素(标题、段落、图片)进行渲染、导出

2.2 不用访问者模式会怎样?

java
// 反例:在每个元素类中硬编码操作
class Book {
    public void calculateTax() { /* 图书税率 */ }
    public void generateReport() { /* 图书报表 */ }
    public void exportToPdf() { /* 导出PDF */ }
    // 每增加一个操作,都要修改 Book 类
}

class Electronic {
    public void calculateTax() { /* 电子产品税率 */ }
    public void generateReport() { /* 电子产品报表 */ }
    public void exportToPdf() { /* 导出PDF */ }
    // 同样的操作需要在每个类中实现
}

问题:

  1. 新增操作需要修改所有元素类,违反开闭原则
  2. 操作逻辑分散在各个元素类中,难以维护
  3. 元素类职责过重,承担了过多的操作逻辑

三、结构

3.1 角色组成

角色说明
Visitor(抽象访问者)定义对每个元素类的访问方法
ConcreteVisitor(具体访问者)实现每个访问方法,定义具体操作
Element(抽象元素)定义 accept() 方法,接受访问者
ConcreteElement(具体元素)实现 accept() 方法,调用访问者的对应方法
ObjectStructure(对象结构)维护元素集合,提供遍历接口

3.2 类图(ASCII)

┌──────────────┐                      ┌──────────────┐
│   Visitor    │                      │   Element    │
├──────────────┤                      ├──────────────┤
│+ visit(ConcreteElementA)│            │+ accept(Visitor)│
│+ visit(ConcreteElementB)│            └──────┬───────┘
└──────┬───────┘                             │
       │                              ┌──────┴──────┐
       │                              │             │
       ▼                              ▼             ▼
┌──────────────┐               ┌──────────┐  ┌──────────┐
│ConcreteVisitor1│              │Concrete  │  │Concrete  │
│ConcreteVisitor2│              │ElementA  │  │ElementB  │
└──────────────┘               └──────────┘  └──────────┘

3.3 双分派机制

访问者模式的核心是双分派(Double Dispatch)

  1. 第一次分派:客户端调用 element.accept(visitor),根据 Element 的类型选择 accept() 方法
  2. 第二次分派:在 accept() 中调用 visitor.visit(this),根据 Visitor 的具体类型选择 visit() 方法
Client → element.accept(visitor)  ──────►  第一次分派(Element 的多态)


         visitor.visit(this)      ──────►  第二次分派(Visitor 的多态)

四、代码实现

4.1 基础实现

java
// ==================== 抽象访问者 ====================
interface ShoppingCartVisitor {
    void visit(Book book);
    void visit(Electronic electronic);
    void visit(Food food);
}

// ==================== 抽象元素 ====================
interface Item {
    void accept(ShoppingCartVisitor visitor);
}

// ==================== 具体元素:图书 ====================
class Book implements Item {
    private String name;
    private double price;
    private String isbn;

    public Book(String name, double price, String isbn) {
        this.name = name;
        this.price = price;
        this.isbn = isbn;
    }

    public String getName() { return name; }
    public double getPrice() { return price; }
    public String getIsbn() { return isbn; }

    @Override
    public void accept(ShoppingCartVisitor visitor) {
        visitor.visit(this); // 双分派:this 的类型是 Book
    }
}

// ==================== 具体元素:电子产品 ====================
class Electronic implements Item {
    private String name;
    private double price;
    private String brand;

    public Electronic(String name, double price, String brand) {
        this.name = name;
        this.price = price;
        this.brand = brand;
    }

    public String getName() { return name; }
    public double getPrice() { return price; }
    public String getBrand() { return brand; }

    @Override
    public void accept(ShoppingCartVisitor visitor) {
        visitor.visit(this); // 双分派:this 的类型是 Electronic
    }
}

// ==================== 具体元素:食品 ====================
class Food implements Item {
    private String name;
    private double price;
    private String expiryDate;

    public Food(String name, double price, String expiryDate) {
        this.name = name;
        this.price = price;
        this.expiryDate = expiryDate;
    }

    public String getName() { return name; }
    public double getPrice() { return price; }
    public String getExpiryDate() { return expiryDate; }

    @Override
    public void accept(ShoppingCartVisitor visitor) {
        visitor.visit(this); // 双分派:this 的类型是 Food
    }
}

// ==================== 具体访问者:计算总价 ====================
class PriceCalculatorVisitor implements ShoppingCartVisitor {
    private double totalPrice = 0;

    @Override
    public void visit(Book book) {
        // 图书打 9 折
        totalPrice += book.getPrice() * 0.9;
    }

    @Override
    public void visit(Electronic electronic) {
        // 电子产品加 10% 税
        totalPrice += electronic.getPrice() * 1.1;
    }

    @Override
    public void visit(Food food) {
        // 食品原价
        totalPrice += food.getPrice();
    }

    public double getTotalPrice() {
        return totalPrice;
    }
}

// ==================== 具体访问者:打印清单 ====================
class PrintVisitor implements ShoppingCartVisitor {
    @Override
    public void visit(Book book) {
        System.out.printf("图书: %s, 价格: ¥%.2f, ISBN: %s\n",
                book.getName(), book.getPrice(), book.getIsbn());
    }

    @Override
    public void visit(Electronic electronic) {
        System.out.printf("电子产品: %s, 价格: ¥%.2f, 品牌: %s\n",
                electronic.getName(), electronic.getPrice(), electronic.getBrand());
    }

    @Override
    public void visit(Food food) {
        System.out.printf("食品: %s, 价格: ¥%.2f, 过期: %s\n",
                food.getName(), food.getPrice(), food.getExpiryDate());
    }
}

// ==================== 对象结构 ====================
class ShoppingCart {
    private List<Item> items = new ArrayList<>();

    public void addItem(Item item) {
        items.add(item);
    }

    public void accept(ShoppingCartVisitor visitor) {
        for (Item item : items) {
            item.accept(visitor);
        }
    }
}

// ==================== 客户端 ====================
public class VisitorDemo {
    public static void main(String[] args) {
        ShoppingCart cart = new ShoppingCart();
        cart.addItem(new Book("设计模式", 59.0, "978-7-111-XXXXX"));
        cart.addItem(new Electronic("iPhone 15", 6999.0, "Apple"));
        cart.addItem(new Food("薯片", 8.5, "2026-12-31"));

        // 打印清单
        System.out.println("=== 购物清单 ===");
        PrintVisitor printer = new PrintVisitor();
        cart.accept(printer);

        // 计算总价
        PriceCalculatorVisitor calculator = new PriceCalculatorVisitor();
        cart.accept(calculator);
        System.out.printf("\n总价: ¥%.2f\n", calculator.getTotalPrice());
    }
}

4.2 进阶实现

4.2.1 静态分派 vs 动态分派

java
// 静态分派(编译时确定)vs 动态分派(运行时确定)
public class DispatchDemo {

    // 静态分派:方法重载(Overload),编译时根据参数类型决定
    static class StaticDispatch {
        void print(int value) { System.out.println("int: " + value); }
        void print(String value) { System.out.println("String: " + value); }
    }

    // 动态分派:方法重写(Override),运行时根据对象类型决定
    static class DynamicDispatch {
        static class Animal { void speak() { System.out.println("Animal"); } }
        static class Dog extends Animal { void speak() { System.out.println("Dog"); } }
        static class Cat extends Animal { void speak() { System.out.println("Cat"); } }
    }

    public static void main(String[] args) {
        // 静态分派
        StaticDispatch sd = new StaticDispatch();
        sd.print(10);      // 编译时确定调用 print(int)
        sd.print("hello"); // 编译时确定调用 print(String)

        // 动态分派(单分派)
        DynamicDispatch.Animal animal = new DynamicDispatch.Dog();
        animal.speak(); // 运行时确定调用 Dog.speak() → "Dog"
    }
}

4.2.2 双分派详解

java
// 双分派:结合了动态分派(Element 的多态)和动态分派(Visitor 的多态)
public class DoubleDispatchDemo {

    interface Element {
        void accept(Visitor visitor); // 第一次分派
    }

    interface Visitor {
        void visit(ElementA element); // 第二次分派
        void visit(ElementB element);
    }

    static class ElementA implements Element {
        @Override
        public void accept(Visitor visitor) {
            visitor.visit(this); // this 的类型是 ElementA
        }
    }

    static class ElementB implements Element {
        @Override
        public void accept(Visitor visitor) {
            visitor.visit(this); // this 的类型是 ElementB
        }
    }

    static class ConcreteVisitor1 implements Visitor {
        @Override
        public void visit(ElementA element) {
            System.out.println("Visitor1 处理 ElementA");
        }
        @Override
        public void visit(ElementB element) {
            System.out.println("Visitor1 处理 ElementB");
        }
    }

    static class ConcreteVisitor2 implements Visitor {
        @Override
        public void visit(ElementA element) {
            System.out.println("Visitor2 处理 ElementA");
        }
        @Override
        public void visit(ElementB element) {
            System.out.println("Visitor2 处理 ElementB");
        }
    }

    public static void main(String[] args) {
        Element element = new ElementA(); // 运行时类型是 ElementA
        Visitor visitor = new ConcreteVisitor1(); // 运行时类型是 ConcreteVisitor1

        // 双分派:
        // 1. 第一次分派:element.accept() → ElementA.accept()
        // 2. 第二次分派:visitor.visit(this) → ConcreteVisitor1.visit(ElementA)
        element.accept(visitor);
    }
}

4.3 生产级实现

Spring Boot 报表生成访问者

java
// ==================== 报表元素 ====================
interface ReportElement {
    void accept(ReportVisitor visitor);
}

// 文本元素
class TextElement implements ReportElement {
    private String text;

    public TextElement(String text) { this.text = text; }
    public String getText() { return text; }

    @Override
    public void accept(ReportVisitor visitor) {
        visitor.visit(this);
    }
}

// 表格元素
class TableElement implements ReportElement {
    private List<String[]> rows = new ArrayList<>();

    public void addRow(String[] row) { rows.add(row); }
    public List<String[]> getRows() { return rows; }

    @Override
    public void accept(ReportVisitor visitor) {
        visitor.visit(this);
    }
}

// 图表元素
class ChartElement implements ReportElement {
    private String title;
    private Map<String, Double> data;

    public ChartElement(String title, Map<String, Double> data) {
        this.title = title;
        this.data = data;
    }
    public String getTitle() { return title; }
    public Map<String, Double> getData() { return data; }

    @Override
    public void accept(ReportVisitor visitor) {
        visitor.visit(this);
    }
}

// ==================== 访问者接口 ====================
interface ReportVisitor {
    void visit(TextElement element);
    void visit(TableElement element);
    void visit(ChartElement element);
}

// ==================== 具体访问者:HTML 生成 ====================
@Component
class HtmlReportVisitor implements ReportVisitor {
    private StringBuilder html = new StringBuilder();

    @Override
    public void visit(TextElement element) {
        html.append("<p>").append(element.getText()).append("</p>\n");
    }

    @Override
    public void visit(TableElement element) {
        html.append("<table border='1'>\n");
        for (String[] row : element.getRows()) {
            html.append("<tr>");
            for (String cell : row) {
                html.append("<td>").append(cell).append("</td>");
            }
            html.append("</tr>\n");
        }
        html.append("</table>\n");
    }

    @Override
    public void visit(ChartElement element) {
        html.append("<div class='chart'>")
            .append("<h3>").append(element.getTitle()).append("</h3>")
            .append("<canvas id='chart'></canvas>")
            .append("</div>\n");
    }

    public String getHtml() { return html.toString(); }
}

// ==================== 具体访问者:PDF 导出 ====================
@Component
class PdfReportVisitor implements ReportVisitor {
    private List<String> pdfContent = new ArrayList<>();

    @Override
    public void visit(TextElement element) {
        pdfContent.add("[TEXT] " + element.getText());
    }

    @Override
    public void visit(TableElement element) {
        pdfContent.add("[TABLE] 行数: " + element.getRows().size());
    }

    @Override
    public void visit(ChartElement element) {
        pdfContent.add("[CHART] " + element.getTitle());
    }

    public List<String> getPdfContent() { return pdfContent; }
}

// ==================== 报表服务 ====================
@Service
class ReportService {
    @Autowired private HtmlReportVisitor htmlVisitor;
    @Autowired private PdfReportVisitor pdfVisitor;

    public String generateHtmlReport(List<ReportElement> elements) {
        for (ReportElement element : elements) {
            element.accept(htmlVisitor);
        }
        return htmlVisitor.getHtml();
    }

    public List<String> generatePdfReport(List<ReportElement> elements) {
        for (ReportElement element : elements) {
            element.accept(pdfVisitor);
        }
        return pdfVisitor.getPdfContent();
    }
}

// ==================== Controller ====================
@RestController
@RequestMapping("/api/reports")
class ReportController {
    @Autowired private ReportService reportService;

    @PostMapping("/html")
    public String generateHtml(@RequestBody List<ReportElement> elements) {
        return reportService.generateHtmlReport(elements);
    }
}

五、优缺点

优点

  1. 符合开闭原则:新增操作(访问者)无需修改元素类,只需新增 Visitor
  2. 单一职责:每个 Visitor 负责一种操作,操作逻辑集中管理
  3. 灵活性:可以轻松添加新的操作,而不影响现有的元素类
  4. 累积状态:访问者可以在遍历过程中累积状态(如计算总价)

缺点

  1. 元素类难以扩展:新增元素类需要修改所有 Visitor 接口和实现
  2. 破坏封装:Visitor 需要访问元素的内部状态(通过 getter)
  3. 元素类必须支持访问者:元素类需要预留 accept() 方法
  4. 双分派复杂:理解双分派机制需要一定学习成本

六、适用场景

  1. 编译器设计:对 AST 的不同节点进行类型检查、代码生成、优化
  2. 报表生成:对报表元素生成 HTML、PDF、Excel 等不同格式
  3. 文档处理:对文档元素进行格式化、导出、统计
  4. 电商系统:对不同商品类型计算税费、折扣、积分
  5. 静态分析工具:代码度量、代码规范检查

七、JDK / Spring 框架中的实际应用

框架应用位置说明
JDKjava.nio.file.FileVisitor文件树遍历访问者
JDKjavax.lang.model.element.AnnotationValueVisitor注解值访问者
JDKjavax.lang.model.element.ElementVisitor程序元素访问者
SpringBeanDefinitionVisitorBean 定义访问者
SpringPropertyPlaceholderHelper属性占位符解析
ASMClassVisitor字节码操作框架

八、与其他模式的关系

与迭代器模式

  • 访问者模式通常与迭代器模式结合使用,迭代器遍历元素集合,访问者处理每个元素

与组合模式

  • 访问者模式常用于处理组合模式构建的树形结构(如 AST)

与解释器模式

  • 访问者模式可以在不修改表达式类的情况下,为解释器模式增加新的操作

与策略模式

  • 访问者模式可以看作是对一组对象的策略模式,每个 Visitor 是一种策略

九、面试常见问题

Q1:什么是双分派(Double Dispatch)?为什么访问者模式需要双分派?

A:双分派是指通过两次方法调用(两次动态分派)来确定最终执行的方法。访问者模式中,第一次分派:element.accept(visitor) 根据 Element 的运行时类型调用对应的 accept() 方法;第二次分派:在 accept() 中调用 visitor.visit(this),根据 Visitor 的运行时类型和 this 的编译时类型(重载)确定最终的 visit() 方法。Java 是单分派语言(只根据接收者类型分派),双分派需要两次调用才能实现。

Q2:访问者模式最大的缺点是什么?

A:新增元素类非常困难。因为 Visitor 接口中需要为每种元素类型声明 visit() 方法,新增元素类意味着所有 Visitor 实现类都需要修改。所以访问者模式适用于元素类层次结构稳定,但需要频繁增加新操作的场景。如果元素类经常变化,应该使用其他模式。

Q3:访问者模式中,元素类为什么需要提供 getter 方法?

A:因为 Visitor 需要访问元素的内部状态来执行操作。如果元素类不提供 getter,Visitor 无法获取需要的数据。这确实在一定程度上破坏了封装性,但这是访问者模式的固有权衡:用封装性换取操作的可扩展性。可以通过将 getter 声明为包级私有限制访问范围。

Q4:Java 中 FileVisitor 是如何使用访问者模式的?

A:java.nio.file.FileVisitor 是访问者模式在文件遍历中的应用。Files.walkFileTree() 遍历文件树,每遇到一个文件/目录就调用 FileVisitor 的对应方法:preVisitDirectory()(进入目录前)、visitFile()(访问文件)、visitFileFailed()(访问失败)、postVisitDirectory()(离开目录后)。这是一个典型的访问者 + 迭代器组合应用。

Q5:访问者模式中,访问者应该是有状态还是无状态的?

A:根据需要。有状态的访问者可以在遍历过程中累积结果(如计算总价、收集统计信息),无状态的访问者每次访问都是独立的(如打印、格式化)。通常访问者是有状态的,因为这是访问者模式的一个重要优势:在遍历过程中收集和聚合信息。