访问者模式:分离数据结构与操作的设计模式
访问者模式:分离数据结构与操作的设计模式
一、模式核心:将操作从数据结构中分离,支持动态添加新操作
在软件开发中,当数据结构(如树、集合)中的元素类型固定,但需要频繁添加新的操作(如遍历、统计、打印)时,直接修改元素类会违反开闭原则。
访问者模式(Visitor Pattern) 将数据结构与作用于结构上的操作分离,使得操作可以独立于数据结构进行扩展。通过引入访问者(Visitor)角色,客户端可以在不修改数据结构的前提下,新增对数据的操作。核心解决:
- 操作扩展问题:新增操作时无需修改数据结构类,符合开闭原则。
- 职责分离:数据结构仅负责存储数据,操作逻辑集中在访问者类中。
- 复杂操作封装:将跨元素的复杂操作(如树的深度优先遍历 + 统计)封装在访问者中,避免污染数据结构。
核心思想与 UML 类图(PlantUML 语法)
访问者模式包含以下角色:
- 抽象访问者(Visitor):定义对每种元素的访问接口。
- 具体访问者(Concrete Visitor):实现抽象访问者接口,定义对具体元素的操作。
- 抽象元素(Element):定义接受访问者的接口(
accept(Visitor)
)。 - 具体元素(Concrete Element):实现接受访问者的方法,调用访问者的对应操作。
- 对象结构(Object Structure):管理元素集合,允许访问者遍历所有元素。
二、核心实现:员工数据统计与可视化
1. 定义抽象元素(员工)
public interface Employee { void accept(Visitor visitor); // 接受访问者 String getName(); double getSalary();
}
2. 实现具体元素(全职员工、兼职员工)
全职员工(有奖金)
public class FullTimeEmployee implements Employee { private String name; private double baseSalary; private double bonus; public FullTimeEmployee(String name, double baseSalary, double bonus) { this.name = name; this.baseSalary = baseSalary; this.bonus = bonus; } @Override public void accept(Visitor visitor) { visitor.visitFullTimeEmployee(this); // 调用访问者的对应方法 } // Getter 方法 public String getName() { return name; } public double getSalary() { return baseSalary + bonus; } public double getBonus() { return bonus; }
}
兼职员工(按小时计费)
public class PartTimeEmployee implements Employee { private String name; private double hourlyRate; private int hoursWorked; public PartTimeEmployee(String name, double hourlyRate, int hoursWorked) { this.name = name; this.hourlyRate = hourlyRate; this.hoursWorked = hoursWorked; } @Override public void accept(Visitor visitor) { visitor.visitPartTimeEmployee(this); // 调用访问者的对应方法 } // Getter 方法 public String getName() { return name; } public double getSalary() { return hourlyRate * hoursWorked; } public int getHoursWorked() { return hoursWorked; }
}
3. 定义抽象访问者(统计与打印)
public interface Visitor { // 访问全职员工 void visitFullTimeEmployee(FullTimeEmployee employee); // 访问兼职员工 void visitPartTimeEmployee(PartTimeEmployee employee);
}
4. 实现具体访问者(薪资统计、员工信息打印)
薪资统计访问者
public class SalaryVisitor implements Visitor { private double totalSalary = 0; @Override public void visitFullTimeEmployee(FullTimeEmployee employee) { totalSalary += employee.getSalary(); System.out.println("全职员工 " + employee.getName() + " 薪资:" + employee.getSalary()); } @Override public void visitPartTimeEmployee(PartTimeEmployee employee) { totalSalary += employee.getSalary(); System.out.println("兼职员工 " + employee.getName() + " 薪资:" + employee.getSalary()); } public double getTotalSalary() { return totalSalary; }
}
员工信息打印访问者
public class InfoPrintVisitor implements Visitor { @Override public void visitFullTimeEmployee(FullTimeEmployee employee) { System.out.println("全职员工信息:"); System.out.println("姓名:" + employee.getName()); System.out.println("基本工资:" + employee.getSalary() - employee.getBonus()); System.out.println("奖金:" + employee.getBonus()); } @Override public void visitPartTimeEmployee(PartTimeEmployee employee) { System.out.println("兼职员工信息:"); System.out.println("姓名:" + employee.getName()); System.out.println("工作时长:" + employee.getHoursWorked() + " 小时"); System.out.println("时薪:" + employee.getHourlyRate()); }
}
5. 对象结构(员工列表)
import java.util.ArrayList;
import java.util.List; public class EmployeeManager { private List<Employee> employees = new ArrayList<>(); public void addEmployee(Employee employee) { employees.add(employee); } // 接受访问者,遍历所有员工 public void accept(Visitor visitor) { for (Employee employee : employees) { employee.accept(visitor); } }
}
6. 客户端使用访问者模式
public class ClientDemo { public static void main(String[] args) { EmployeeManager manager = new EmployeeManager(); manager.addEmployee(new FullTimeEmployee("Alice", 8000, 2000)); manager.addEmployee(new PartTimeEmployee("Bob", 50, 160)); // 统计薪资 SalaryVisitor salaryVisitor = new SalaryVisitor(); manager.accept(salaryVisitor); System.out.println("\n总薪资:" + salaryVisitor.getTotalSalary()); // 打印员工信息 InfoPrintVisitor printVisitor = new InfoPrintVisitor(); manager.accept(printVisitor); }
}
输出结果:
全职员工 Alice 薪资:10000.0
兼职员工 Bob 薪资:8000.0 总薪资:18000.0 全职员工信息:
姓名:Alice
基本工资:8000.0
奖金:2000.0
兼职员工信息:
姓名:Bob
工作时长:160 小时
时薪:50.0
三、进阶:双分派与类型安全
访问者模式通过双分派(Double Dispatch) 实现类型安全的操作:
- 第一分派:元素调用
accept(visitor)
时,根据元素类型确定调用哪个accept
实现。 - 第二分派:在
accept
方法中,调用visitor.visitXXX(this)
,根据访问者类型和元素类型确定具体操作。
// 元素的 accept 方法(第一分派)
public class FullTimeEmployee implements Employee { @Override public void accept(Visitor visitor) { visitor.visitFullTimeEmployee(this); // 第二分派:传入当前元素实例 }
}
四、框架与源码中的访问者实践
1. Java 编译器(如 Eclipse JDT)
Java 编译器使用访问者模式遍历抽象语法树(AST),实现词法分析、语法分析和语义分析。例如,ASTVisitor
类定义了对各种节点(如 MethodDeclaration
、VariableDeclaration
)的访问方法。
2. XML/JSON 解析器
解析器将 XML 节点(如 <user>
、<order>
)作为元素,访问者实现对节点的验证、转换或统计操作(如验证订单节点的金额是否合法)。
3. Apache Ant 构建工具
Ant 通过访问者模式遍历构建文件(build.xml
)中的任务节点(如 <copy>
、<delete>
),执行对应的构建操作。
五、避坑指南:正确使用访问者模式的 3 个要点
1. 数据结构稳定性优先
访问者模式适用于数据结构稳定、操作频繁变化的场景。若数据结构经常新增元素类型,需修改所有访问者,违反开闭原则。
2. 避免过度使用双分派
双分派会增加代码复杂度,需确保访问者与元素的类型组合不会导致类爆炸(如元素类型 n,访问者类型 m,需实现 n×m 个方法)。
3. 处理跨元素操作的一致性
若访问者需要维护跨元素的状态(如统计总和),需在访问者中设计状态管理逻辑,避免状态泄漏到数据结构中。
六、总结:何时该用访问者模式?
适用场景 | 核心特征 | 典型案例 |
---|---|---|
数据结构固定,操作多变 | 元素类型不变,但需频繁新增操作 | 编译器优化、文档格式转换(如 Markdown 转 HTML) |
跨元素复杂操作 | 操作需要遍历多种元素并执行不同逻辑 | 电商订单统计(同时处理商品、物流、支付元素) |
操作与数据解耦 | 操作逻辑与数据存储分离,独立维护 | 财务系统(数据存储在数据库,操作包含报表生成、审计) |
访问者模式通过分离数据与操作,为系统提供了强大的扩展能力。下一篇我们将探讨最后一个设计模式 —— 解释器模式,解析如何实现自定义语言的解释器,敬请期待!
扩展思考:访问者模式 vs 策略模式
类型 | 核心差异 | 适用场景 |
---|---|---|
访问者模式 | 操作针对不同元素类型(多态分派) | 数据结构固定,操作动态扩展 |
策略模式 | 操作是同接口的不同实现(算法切换) | 同一元素的不同处理策略 |