访问者模式 Visitor
一. 根据实际案例分析传统模式中可能存在的问题
- 案例: 现有员工Employee, 管理员工ManagementEmployee, 需求,对每个员工增加奖金,每个员工的等级*100,是奖金的算法,计算每个员工的奖金,与奖金+工资
- 员工类
class Employee{
public String name; //姓名
public int salary; //薪资
public Integer grade; //等级
public Employee(String name, int salary, int grade){
this.name = name;
this.salary = salary;
this.grade = grade;
}
public String getName(){
return name;
}
public int getGrade(){
return grade;
}
public int getSalary(){
return salary;
}
public void setSalary(int salary) {
this.salary = salary;
}
}
- 员工管理类
//管理员工信息类
class ManagementEmployee{
//存放员工容器
public HashMap<String,Employee> employeeMap = new HashMap<>();
//添加员工
public void attach(Employee employee){
employeeMap.put(employee.getName(), employee);
}
//删除员工
public void detach(Employee employee){
employeeMap.remove(employee);
}
//对每个员工增加奖金的算法
public void bonus(){
employeeMap.entrySet().stream().forEach(m -> {
Employee e = m.getValue();
int add = e.getGrade()*100;
e.setSalary(e.getSalary()+add);
System.out.println("员工"+e.getName()
+"增加奖金"+add+",总工资为"+e.getSalary());
} );
}
}
- 客户端调用测试
public static void main(String[] args) {
//创建不同员工
Employee e1 = new Employee("小明",2000,2);
Employee e2 = new Employee("小红", 1800, 2);
Employee e3 = new Employee("小刘", 3500, 3);
Employee e4 = new Employee("小强", 1500, 1);
//将员工添加到管理系统
ManagementEmployee management = new ManagementEmployee();
management.attach(e1);
management.attach(e2);
management.attach(e3);
management.attach(e4);
//调用奖金算法,计算员工奖金与奖金+工资
management.bonus();
}
- 分析可能存在的问题: 调用ManagementEmployee中的bonus()方法计算员工奖金,假设后续奖金算法改变了怎么办?要根据需求重新修改bonus()方法,违反ocp原则
- 使用访问者模式解决问题: 将可能变化的算法抽取出来,创建出一个访问对象,当我们改变奖金算法时,增加或修改访问对象即可
- 什么叫访问者: 先了解什么叫访问 在数据结构方面来看遍历就是访问的一般形式,单独读取一个元素进行相应的处理也叫作访问,读取到想要查看的内容+对其进行处理就叫做访问,在此处,调用奖金算法方法bonus(),就是一种访问,将访问抽取出来创建为一个可以执行的个体,就是访问者
二. 访问者模式讲解与示例
- 访问者模式: 对于一组对象,在不改变数据结构的情况下(数据结构此处是指Employee中各个属性)增加操作这些结构元素的新功能,把数据结构与操作数据的方法进行解耦(奖金与计算奖金的算法进行剥离),使操作更加灵活自由
- 优点 :
单一职责(可能存在多个访问者,例如多种不同的奖金算法,每个访问者只负责一种)
扩展性良好,抽象出公共的抽象访问者接口,后续需要增加其它算法,实现抽象接口即可 - 缺点: 固定的数据结构情况下使用,例如Employee中的属性不固定,则不推荐使用
- 访问者模式与迭代器模式: 访问者关注的是保存在容器中的元素进行某种指定的处理,迭代器模式关注的是对容器中的元素进行逐个遍历
使用访问者模式修改案例
- 创建抽象访问者,创建抽象访问者原因是便于以后添加其它访问者,例如后续增加了另外一种奖金算法,直接实现抽象访问者即可
//抽象访问者
interface Visitor{
//访问方法,声明可以访问那些类型的数据Element被访问者
public void vosit(Element element);
}
- 创建具体访问者,实现访问方法,也就是Employee,计算奖金的方法,抽取出来
//具体访问者
class CompensationVisitor implements Visitor{
//通过外部传递进来被访问者,拿到被访问者,根据需求实现奖金的算法
//访问者具体访问到一种元素后该干什么
@Override
public void vosit(Element element) {
Employee e = (Employee) element;
int add = e.getGrade()*100;
e.setSalary(e.getSalary()+add);
System.out.println("员工"+e.getName()
+"增加奖金"+add+",总工资为"+e.getSalary());
}
}
- 创建抽象被访问者,声明被声明类型的访问者访问,抽象的原因是便于后续扩展,假设,后续还有其他类型的员工,直接继承即可
//抽象被访问者
interface Element{
//提供接收访问者方法,被什么元素访问Visitor
public void accept(Visitor visitor);
}
- 创建具体被访问者,实现抽象访问者,注意被访问者与访问者的交互方法
//具体被访问者,继承被访问者接口
class Employee implements Element{
public String name; //姓名
public int salary; //薪资
public Integer grade; //等级
public Employee(String name, int salary, int grade){
this.name = name;
this.salary = salary;
this.grade = grade;
}
public String getName(){
return name;
}
public int getGrade(){
return grade;
}
public int getSalary(){
return salary;
}
public void setSalary(int salary) {
this.salary = salary;
}
//重点: 重写接收访问者方法,接收到访问者后怎么操作
//在访问者中定义访问方法,而访问方法要访问的数据,
//正是当前类对象类对象类型数据(Employee是Element实现子类)
@Override
public void accept(Visitor visitor) {
//通过方法传递进来的访问者,调用访问方法,而访问的数据正是当前
//类型数据,将当前类对象this传递给访问者即可
visitor.vosit(this);
}
}
- 创建员工管理类,元素容器类
//管理员工信息类
class ManagementEmployee{
//存放员工容器
public HashMap<String,Employee> employeeMap = new HashMap<>();
//添加员工
public void attach(Employee employee){
employeeMap.put(employee.getName(), employee);
}
//删除员工
public void detach(Employee employee){
employeeMap.remove(employee);
}
//遍历容器中被访问元素,传递访问者对象,调用访问方法
public void accept(Visitor visitor){
employeeMap.entrySet().stream()
.forEach(eMap -> eMap.getValue().accept(visitor));
}
}
- 调用测试
public static void main(String[] args) {
//创建不同员工
Employee e1 = new Employee("小明",2000,2);
Employee e2 = new Employee("小红", 1800, 2);
Employee e3 = new Employee("小刘", 3500, 3);
Employee e4 = new Employee("小强", 1500, 1);
//将员工添加到管理系统
ManagementEmployee management = new ManagementEmployee();
management.attach(e1);
management.attach(e2);
management.attach(e3);
management.attach(e4);
//调用奖金算法,计算员工奖金与奖金+工资
//management1.bonus();
CompensationVisitor compensationVisitor = new CompensationVisitor();
management.accept(compensationVisitor);
}
访问者模式角色分析
- 抽象访问者 Visitor : 也就是此处的Visitor接口,声明访问者可以访问哪类元素,为了方便后续扩展提供抽象接口,后续对当前元素增加访问者创建实现子类即可
- 具体访问者 ConcreteVisitor : 实现抽象访问者所声明的方法(如例上CompensationVisitor ),它影响到访问者具体访问到一种元素后该干什么,要做什么事情
- 抽象被访问者 Element : 一般为抽象类或者接口(如上例Element),声明接受哪一类访问者访问,程序上是通过accept方法中的参数来定义的
- 具体被访问者 ConcreteElement : 实现抽象元素类所声明的accept方法(如上例Employee),通常都是以this指针为实参,基本上已经形成一种定式。
- 元素对象容器 : 一个元素的容器(如上例ManagementEmployee, 有的项目中也可能没有这个角色,根据实现需求定义),一般包含一个容纳多个不同类、不同接口的容器,如List、Set、Map等,在项目中一般很少抽象出这个角色。
流程图
Spring 中访问者模式的使用案例
在 Spring IOC中,查看 BeanDefinitionVisitor ,Bean的访问者,可以遍历Bean的属性,使用properties进行填充
业务与设计模式落地案例
- 访问者模式的优点在于能够将数据结构与操作分离,从而使得可以很方便地增加新的操作,而不需要修改现有的数据结构
- 一个学生信息管理系统为例,演示如何使用访问者模式实现查询、添加和删除学生信息的功能
- 定义学生信息类 Student
@Data
@AllArgsConstructor
public class Student {
private String id;
private String name;
private int age;
}
- 定义学生信息仓库类 StudentRepository,用于存储学生信息并提供查询、添加和删除学生信息的接口
@Component
public class StudentRepository {
private List<Student> students = new ArrayList<>();
public void add(Student student) {
students.add(student);
}
public Student get(String id) {
return students.stream()
.filter(s -> s.getId().equals(id))
.findFirst()
.orElse(null);
}
public void remove(String id) {
students.removeIf(s -> s.getId().equals(id));
}
}
- 定义访问者接口 Visitor,用于访问学生信息并执行操作
public interface Visitor {
//用于访问学生信息并执行操作
void visit(Student student);
}
- 定义具体访问者 StudentVisitor,用于执行具体的操作
@Component
public class StudentVisitor implements Visitor {
//注入了 StudentRepository 类
private final StudentRepository repository;
public StudentVisitor(StudentRepository repository) {
this.repository = repository;
}
//在 visit 方法中调用 get 方法查询学生信息并进行处理
@Override
public void visit(Student student) {
// 查询学生信息
Student result = repository.get(student.getId());
if (result != null) {
log.info("查询到学生信息:{}", result);
} else {
log.info("未查询到该学生信息");
}
}
}
- 定义被访问者接口 Element,用于接受访问者的访问
public interface Element {
//用于接受访问者的访问并执行操作
void accept(Visitor visitor);
}
- 定义具体被访问者 StudentElement,用于存储学生信息并将其暴露给访问者
@Data
@AllArgsConstructor
public class StudentElement implements Element {
private Student student;
//在 accept 方法中将其暴露给访问者
@Override
public void accept(Visitor visitor) {
visitor.visit(student);
}
}
- 接收请求
@RestController
@RequestMapping("/student")
public class StudentController {
private final StudentRepository repository;
private final StudentVisitor visitor;
public StudentController(StudentRepository repository,
StudentVisitor visitor) {
this.repository = repository;
this.visitor = visitor;
}
@GetMapping("/{id}")
public String getStudent(@PathVariable String id) {
// 查询学生信息
Student student = repository.get(id);
if (student != null) {
log.info("查询到学生信息:{}", student);
return "查询到学生信息:" + student.toString();
} else {
log.info("未查询到该学生信息");
return "未查询到该学生信息";
}
}
@PostMapping("/add")
public String addStudent(@RequestBody Student student) {
// 添加学生信息
repository.add(student);
log.info("添加学生信息:{}", student);
return "添加学生信息成功";
}
@DeleteMapping("/{id}")
public String removeStudent(@PathVariable String id) {
// 删除学生信息
repository.remove(id);
log.info("删除学生信息,id={}", id);
return "删除学生信息成功";
}
@GetMapping("/visitor/{id}")
public String visitStudent(@PathVariable String id) {
// 访问学生信息
Student student = repository.get(id);
if (student != null) {
StudentElement element = new StudentElement(student);
log.info("访问学生信息:{}", student);
element.accept(visitor);
return "访问学生信息成功";
} else {
log.info("未查询到该学生信息");
return "未查询到该学生信息";
}
}
}