欢迎光临
我们一直在努力

数据结构稳定但操作总在变,访问者模式就是为此而生

数据结构稳定但操作总在变,访问者模式就是为此而生

你有一个类层次结构:员工有普通员工、经理、总监三种角色。现在财务要算所有人的工资,HR 要统计所有人的工龄,行政部门要检查所有人的出差记录。

这三个操作都需要遍历这三种角色,而每个角色的计算逻辑都不一样。你的选择是什么?

方案 A:在每种角色里分别加 calcSalary()、getWorkYears()、checkTravelRecord() 三个方法。角色类越来越多方法,越来越膨胀。

方案 B:写三个 Service,每个 Service 里 if-else 判断类型。加一种新角色就要改三个地方,改漏了就出 Bug。

访问者模式(Visitor Pattern)给的是第三条路。

访问者模式的核心思想

访问者模式的核心判断标准就一个:你的数据结构稳定不变,但在它上面的操作频繁变化。

这时候你把"操作"从数据结构里抽出来,独立成一个新的类层次——访问者。数据结构(被访问者)只负责"接受"访问者,具体的操作逻辑由访问者自己决定。

结构是这样的:

``` Element(元素接口) ├── ConcreteElementA └── ConcreteElementB

Visitor(访问者接口) ├── ConcreteVisitorA(操作 A 的逻辑) └── ConcreteVisitorB(操作 B 的逻辑) ```

关键点:Visitor 接口里对每种 ConcreteElement 都有一个对应的 visit() 方法。这就是所谓的双重分派——运行时既确定了元素的类型,又确定了访问者的类型。

直接上代码:员工薪资计算

用前面的员工例子来说。

元素(被访问者)这边:

```java // 元素接口 public interface Employee { void accept(EmployeeVisitor visitor); }

// 普通员工 public class Staff implements Employee { private String name; private int baseSalary; private int overtimeHours;

@Override
public void accept(EmployeeVisitor visitor) {
visitor.visit(this); // 注意传的是 this
}

// getters…

}

// 经理 public class Manager implements Employee { private String name; private int baseSalary; private int teamSize;

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

// getters…

}

// 总监 public class Director implements Employee { private String name; private int baseSalary; private double bonusRate;

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

// getters…

} ```

访问者这边:

```java public interface EmployeeVisitor { void visit(Staff staff); void visit(Manager manager); void visit(Director director); }

// 薪资计算访问者 public class SalaryVisitor implements EmployeeVisitor { private int totalSalary = 0;

@Override
public void visit(Staff staff) {
int salary = staff.getBaseSalary() + staff.getOvertimeHours() * 80;
totalSalary += salary;
System.out.println("Staff " + staff.getName() + ": " + salary);
}

@Override
public void visit(Manager manager) {
int salary = manager.getBaseSalary() + manager.getTeamSize() * 500;
totalSalary += salary;
System.out.println("Manager " + manager.getName() + ": " + salary);
}

@Override
public void visit(Director director) {
int salary = (int)(director.getBaseSalary() * (1 + director.getBonusRate()));
totalSalary += salary;
System.out.println("Director " + director.getName() + ": " + salary);
}

public int getTotalSalary() {
return totalSalary;
}

} ```

使用的时候:

```java List

employees = new ArrayList<>(); employees.add(new Staff("张三", 5000, 20)); employees.add(new Manager("李四", 8000, 5)); employees.add(new Director("王五", 15000, 0.3));

SalaryVisitor visitor = new SalaryVisitor(); for (Employee e : employees) { e.accept(visitor); } System.out.println("Total: " + visitor.getTotalSalary()); ```

如果明天 HR 要加一个绩效评估的操作,你不需要改任何 Employee 类。新建一个 PerformanceVisitor 就行了:

```java public class PerformanceVisitor implements EmployeeVisitor { @Override public void visit(Staff staff) { System.out.println(staff.getName() + " 绩效考核: KPI 达成率"); }

@Override
public void visit(Manager manager) {
System.out.println(manager.getName() + " 绩效考核: 团队业绩 + 管理能力");
}

@Override
public void visit(Director director) {
System.out.println(director.getName() + " 绩效考核: 战略目标达成 + 部门整体表现");
}

} ```

数据结构完全没动,新操作只加了新的 Visitor 类。

双重分派是怎么回事

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

Java 是单分派语言——方法重载在编译时就确定了调用哪个版本。所以如果你这样写:

java public void process(Employee e) { // 编译时只知道 e 是 Employee 类型,不知道是 Staff 还是 Manager // 所以只能调用 Employee 的方法,不能做类型判断 }

但访问者模式通过 e.accept(visitor) + visitor.visit(this) 两步绕过了这个限制:

  • e.accept(visitor) —— 根据 e 的实际类型(Staff/Manager/Director)调用对应的 accept 实现
  • visitor.visit(this) —— 此时 this 的实际类型已经确定了,编译器能正确选择 visitor 的重载方法
  • 两步加起来,就实现了运行时的双重分派。

    一个真实踩坑经历

    我之前在一个审批系统里用了访问者模式。审批节点有好几种:开始节点、结束节点、条件分支、并行网关、人工审批节点……每种节点的验证规则不同,序列化逻辑不同,执行逻辑也不同。

    我一开始用访问者模式做了验证和执行两个 Visitor,感觉挺优雅。后来产品经理加了个需求:有些节点需要支持子流程引用。这就需要加一种新的节点类型——子流程节点。

    问题来了:加一种新节点类型,意味着所有 Visitor 都要加一个 visit(SubProcessNode node) 方法。当时我有 5 个 Visitor,就得改 5 个文件。更麻烦的是,有些 Visitor 被打成 JAR 包供其他模块使用,改了就涉及版本兼容。

    这就是访问者模式最大的硬伤:加新的元素类型很困难。数据结构和访问者是互相依赖的,加新元素要改所有访问者,加新访问者要改所有元素。

    所以这个模式有一个非常严格的使用前提:你的元素类型必须稳定,不会频繁增加。如果元素类型也在变,别用访问者模式,会让你痛不欲生。

    什么时候该用、什么时候别用

    用访问者模式需要同时满足三个条件:

  • 对象结构稳定,新增类型的情况很少
  • 对这些对象有很多不同的且不相关的操作
  • 这些操作需要访问对象的私有数据(或者说操作逻辑不应该属于对象本身)
  • 不满足这些条件的,老老实实用组合模式或者策略模式就好。

    如果操作类型在变、对象类型也在变,访问者模式是最差选择。这时候考虑:

    • 组合模式 + 策略模式:把操作封装成策略,对象组合成树,两者独立变化
    • 多分派模拟:如果类型不多,用 Map 注册处理器也够用

    访问者模式在日常业务开发里用得确实不多,但它在编译器(AST 节点遍历)、文档处理(XML/JSON 节点操作)、规则引擎等领域非常常见。如果你在做一个代码生成工具或者文档转换引擎,大概率会遇到它。

    理解访问者模式最大的收获不是"以后要多用它",而是理解双重分派和操作与数据分离的思路。这个思路在很多框架设计里都有体现。


    我在做一个用卡皮巴拉讲设计模式的微信小程序「爪爪代码冒险记」,23 个设计模式用漫画 + 答题的方式讲,目前正在开发中。如果你觉得这类内容有意思,搜一下「爪爪代码冒险记」,或者等我后面的文章——每篇文章我都会同步对应的小程序内容。

    赞(0)
    未经允许不得转载:171主机测评 » 数据结构稳定但操作总在变,访问者模式就是为此而生
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址