一、访问者模式(Visitor Pattern

1、模式概述

访问者模式 是一种 行为型设计模式,它将 数据结构作用于该结构上的操作 分离。通过引入一个 访问者(Visitor),让其“访问”结构中的每个元素,并根据元素类型执行特定操作。

核心思想“结构稳定,操作可变 —— 新增行为不改结构。”

访问者模式 = 结构固定 + 操作外置

  • 它不是“把逻辑塞进对象”,而是“让对象主动邀请外部逻辑来处理自己”。

类比理解:

  • 你在朋友家做客(你是 访问者);
  • 朋友向你展示他的收藏(数据结构);
  • 你根据每件物品(元素)做出不同评价(处理逻辑)。
  • 朋友不需要知道你会怎么评价,只需“接受你的访问”即可。

2、使用场景

访问者模式适用于以下情况:

  • 不希望 污染元素类 的代码(避免在每个元素中写大量业务逻辑);
  • 需要对 一组不同类型的对象 执行 相同的操作流程,但具体行为因类型而异;
  • 符合 开闭原则:新增操作只需添加新访问者,无需修改现有元素类。

📌 典型应用

  • 编译器中的语法树遍历(AST Visitor);
  • 报表生成(同一数据结构 → HTML / PDF / Excel 多种输出);
  • 游戏中对不同单位(士兵、建筑、资源)执行统一 AI 行为;
  • 电商订单系统中对商品、优惠券、运费等组件进行统一计价。

4、示例程序:电脑部件 + 多种访问者

1)元素接口 ComputerPart

/**
 * 元素接口(Element)
 * 所有可被访问的部件都需实现此接口
 */
public interface ComputerPart {
    
    void accept(ComputerPartVisitor visitor);
}

2)具体元素(ConcreteElement

a、CPU

public class CPU implements ComputerPart {
    
   @Override
    public void accept(ComputerPartVisitor visitor) {
        visitor.visit(this); // 双重分发:调用 visitor.visit(CPU)
    }
}

b、HardDisk

public class HardDisk implements ComputerPart {
    
   @Override
    public void accept(ComputerPartVisitor visitor) {
        visitor.visit(this);
    }
}

c、Monitor

public class Monitor implements ComputerPart {
   
    @Override
    public void accept(ComputerPartVisitor visitor) {
        visitor.visit(this);
    }
}

3)访问者接口 ComputerPartVisitor

  • 接口方法 按具体类型重载,而非泛型或 Object;
  • 利用编译时类型信息,实现精准分发。
/**
 * 访问者接口(Visitor)
 * 为每种具体元素声明 visit 方法
 */
public interface ComputerPartVisitor {
    void visit(CPU cpu);
    void visit(HardDisk hardDisk);
    void visit(Monitor monitor);
}

4)具体访问者(ConcreteVisitor

a、显示访问者

/**
 * 具体访问者:显示部件信息
 */
public class DisplayVisitor implements ComputerPartVisitor {
    
    @Override
    public void visit(CPU cpu) {
        System.out.println("Displaying CPU");
    }

    @Override
    public void visit(HardDisk hardDisk) {
        System.out.println("Displaying Hard Disk");
    }

    @Override
    public void visit(Monitor monitor) {
        System.out.println("Displaying Monitor");
    }
}

b、价格计算访问者

/**
 * 具体访问者:计算总价格
 */
public class PriceCalculator implements ComputerPartVisitor {
    private double total = 0;

    @Override
    public void visit(CPU cpu) {
        total += 3000;
        System.out.println("CPU added: ¥3000");
    }

    @Override
    public void visit(HardDisk hardDisk) {
        total += 800;
        System.out.println("Hard Disk added: ¥800");
    }

    @Override
    public void visit(Monitor monitor) {
        total += 1500;
        System.out.println("Monitor added: ¥1500");
    }

    public double getTotal() {
        return total;
    }
}

5)对象结构(可选)Computer

import java.util.Arrays;
import java.util.List;

/**
 * 对象结构(ObjectStructure)
 * 管理所有部件,并提供统一访问入口
 */
public class Computer {
    private final List<ComputerPart> parts = Arrays.asList(
        new CPU(),
        new HardDisk(),
        new Monitor()
    );

    public void accept(ComputerPartVisitor visitor) {
        for (ComputerPart part : parts) {
            part.accept(visitor);
        }
    }
}

6)客户端测试 Main

  • 新增操作(如 PowerConsumptionVisitor无需修改任何部件类
  • 同一结构支持 无限种访问行为
  • 逻辑集中,易于维护。
public class Main {
    public static void main(String[] args) {
        Computer computer = new Computer();

        // 场景1:显示所有部件
        System.out.println("=== 显示部件 ===");
        computer.accept(new DisplayVisitor());

        // 场景2:计算总价
        System.out.println("\n=== 计算价格 ===");
        PriceCalculator calculator = new PriceCalculator();
        computer.accept(calculator);
        System.out.println("总价: ¥" + calculator.getTotal());
    }
}

输出:

=== 显示部件 ===
Displaying CPU
Displaying Hard Disk
Displaying Monitor

=== 计算价格 ===
CPU added: ¥3000
Hard Disk added: ¥800
Monitor added: ¥1500
总价: ¥5300

7)模式角色

角色 职责 示例
Visitor(访问者接口) 声明访问各元素的方法 ComputerPartVisitor
ConcreteVisitor(具体访问者) 实现访问者接口,定义具体操作 DisplayVisitor, PriceCalculator
Element(元素接口) 声明接受访问者的方法 ComputerPart
ConcreteElement(具体元素) 实现 Element 接口,调用访问者的对应方法 CPU, HardDisk, Monitor
ObjectStructure(对象结构) 管理元素集合,提供高层遍历接口(可选) Computer
                ┌──────────────────────┐
                │  ComputerPart        │
                │----------------------│
                │ + accept(visitor)    │
                └──────────▲───────────┘
                           │
     ┌─────────────────────┴─────────────────────┐
     │                                           │
┌────▼─────┐   ┌───────────▼───────────┐   ┌─────▼──────┐
│   CPU    │   │      HardDisk         │   │   Monitor  │
└──────────┘   └───────────────────────┘   └────────────┘

                ┌──────────────────────┐
                │ ComputerPartVisitor  │
                │----------------------│
                │ + visit(CPU)         │
                │ + visit(HardDisk)    │
                │ + visit(Monitor)     │
                └──────────▲───────────┘
                           │
       ┌───────────────────┴───────────────────┐
       │                                       │
┌──────▼────────┐                   ┌──────────▼───────────┐
│ DisplayVisitor│                   │ PriceCalculator      │
└───────────────┘                   └──────────────────────┘

5、FQA

1) 模式优点

  • 符合开闭原则:新增操作只需添加新访问者;
  • 职责分离:元素只负责“被访问”,操作逻辑集中在访问者;
  • 易于增加新操作:特别适合“操作多、结构稳定”的系统。

2)注意事项

  • 破坏封装性:访问者需了解元素内部细节;
  • 难以扩展元素:新增元素类型需修改所有访问者接口(违反开闭原则);
  • 适用范围有限:仅当 结构稳定、操作多变 时才推荐使用。