访问者模式
# 定义
在访问者模式(Visitor Pattern)中,我们使用了一个访问者类,它改变了元素类的执行算法。通过这种方式,元素的执行算法可以随着访问者改变而改变。这种类型的设计模式属于行为型模式。根据模式,元素对象已接受访问者对象,这样访问者对象就可以处理元素对象上的操作。
访问者要解决的核心事项是,在一个稳定的数据结构下,例如用户信息、雇员信息等,增加易变的业务访问逻辑。为了增强扩展性,将这两部分的业务解耦的一种设计模式。
优点:
1. 符合单一职责原则。
2. 优秀的扩展性。
3. 灵活性。
缺点:
1. 具体元素对访问者公布细节,违反了迪米特原则。
2. 具体元素变更比较困难。
3. 违反了依赖倒置原则,依赖了具体类,没有依赖抽象。
使用场景:
1. 对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。
2. 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,也不希望在增加新操作时修改这些类。
# 实践
本次模拟校园中的学生和老师对于不同用户的访问视角
>这个案例场景我们模拟校园中有学生和老师两种身份的用户,那么对于家长和校长关心的角度来看,他们的视角是不同的。家长更关心孩子的成绩和老师的能力,校长更关心老师所在班级学生的人数和升学率{此处模拟的}。
>
>那么这样学生和老师就是一个固定信息的内容,而想让不同视角的用户获取关心的信息,就比较适合使用观察者模式来实现,从而让实体与业务解耦,增强扩展性。但观察者模式的整体类结构相对复杂,需要梳理清楚再开发
访问者模式的类结构相对其他设计模式来说比较复杂,但这样的设计模式在我看来更加烧气有魅力,它能阔开你对代码结构的新认知,用这样思维不断的建设出更好的代码架构。
关于这个案例的核心逻辑实现,有以下几点:
- 建立用户抽象类和抽象访问方法,再由不同的用户实现;老师和学生。
- 建立访问者接口,用于不同人员的访问操作;校长和家长。
- 最终是对数据的看板建设,用于实现不同视角的访问结果输出。
代码结构:

访问者模式模型结构:

定义用户抽象类
```java
@Data
public abstract class User {
public String name; // 姓名
public String identity; // 身份;重点班、普通班 | 特级教师、普通教师、实习教师
public String clazz; // 班级
public User(String name, String identity, String clazz) {
this.name = name;
this.identity = identity;
this.clazz = clazz;
}
// 核心访问方法
public abstract void accept(Visitor visitor);
}
```
实现用户信息(老师和学生)
老师:
```java
public class Teacher extends User {
public Teacher(String name, String identity, String clazz) {
super(name, identity, clazz);
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
// 升本率
public double entranceRatio() {
return BigDecimal.valueOf(Math.random() * 100).setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();
}
}
```
学生:
```java
public class Student extends User {
public Student(String name, String identity, String clazz) {
super(name, identity, clazz);
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
public int ranking() {
return (int) (Math.random() * 100);
}
}
```
- 这里实现了老师和学生类,都提供了父类的构造函数。
- 在accept方法中,提供了本地对象的访问;visitor.visit(this),这块需要加深理解。
- 老师和学生类又都单独提供了各自的特性方法;升本率(entranceRatio)、排名(ranking),类似这样的方法可以按照业务需求进行扩展。
定义访问数据接口
```java
public interface Visitor {
// 访问学生信息
void visit(Student student);
// 访问老师信息
void visit(Teacher teacher);
}
```
实现访问类型(校长和家长)
家长:
```java
@Slf4j
public class Parent implements Visitor {
@Override
public void visit(Student student) {
log.info("学生信息 姓名:{} 班级:{} 排名:{}", student.name, student.clazz, student.ranking());
}
@Override
public void visit(Teacher teacher) {
log.info("老师信息 姓名:{} 班级:{} 级别:{}", teacher.name, teacher.clazz, teacher.identity);
}
}
```
校长:
```java
@Slf4j
public class Principal implements Visitor {
@Override
public void visit(Student student) {
log.info("学生信息 姓名:{} 班级:{}", student.name, student.clazz);
}
@Override
public void visit(Teacher teacher) {
log.info("学生信息 姓名:{} 班级:{} 升学率:{}", teacher.name, teacher.clazz, teacher.entranceRatio());
}
}
```
- 以上是两个具体的访问者实现类,他们都有自己的视角需求。
- 校长关注;学生的名称和班级,老师对这个班级的升学率
- 家长关注;自己家孩子的排名,老师的班级和教学水平
数据看版
```java
public class DataView {
List<User> userList = new ArrayList<User>();
public DataView() {
userList.add(new Student("谢飞机", "重点班", "一年一班"));
userList.add(new Student("windy", "重点班", "一年一班"));
userList.add(new Student("大毛", "普通班", "二年三班"));
userList.add(new Student("Shing", "普通班", "三年四班"));
userList.add(new Teacher("BK", "特级教师", "一年一班"));
userList.add(new Teacher("娜娜Goddess", "特级教师", "一年一班"));
userList.add(new Teacher("dangdang", "普通教师", "二年三班"));
userList.add(new Teacher("泽东", "实习教师", "三年四班"));
}
// 展示
public void show(Visitor visitor) {
for (User user : userList) {
user.accept(visitor);
}
}
}
```
测试类:
```java
@Slf4j
public class ApiTest {
@Test
public void test(){
DataView dataView = new DataView();
log.info("\r\n家长视角访问:");
dataView.show(new Parent()); // 家长
log.info("\r\n校长视角访问:");
dataView.show(new Principal()); // 校长
}
}
```
测试结果:
```java
16:40:25.035 [main] INFO ApiTest -
家长视角访问:
16:40:25.038 [main] INFO fun.lixj.design.visitor.impl.Parent - 学生信息 姓名:谢飞机 班级:一年一班 排名:69
16:40:25.039 [main] INFO fun.lixj.design.visitor.impl.Parent - 学生信息 姓名:windy 班级:一年一班 排名:26
16:40:25.039 [main] INFO fun.lixj.design.visitor.impl.Parent - 学生信息 姓名:大毛 班级:二年三班 排名:14
16:40:25.039 [main] INFO fun.lixj.design.visitor.impl.Parent - 学生信息 姓名:Shing 班级:三年四班 排名:8
16:40:25.039 [main] INFO fun.lixj.design.visitor.impl.Parent - 老师信息 姓名:BK 班级:一年一班 级别:特级教师
16:40:25.039 [main] INFO fun.lixj.design.visitor.impl.Parent - 老师信息 姓名:娜娜Goddess 班级:一年一班 级别:特级教师
16:40:25.039 [main] INFO fun.lixj.design.visitor.impl.Parent - 老师信息 姓名:dangdang 班级:二年三班 级别:普通教师
16:40:25.039 [main] INFO fun.lixj.design.visitor.impl.Parent - 老师信息 姓名:泽东 班级:三年四班 级别:实习教师
16:40:25.039 [main] INFO ApiTest -
校长视角访问:
16:40:25.039 [main] INFO fun.lixj.design.visitor.impl.Principal - 学生信息 姓名:谢飞机 班级:一年一班
16:40:25.039 [main] INFO fun.lixj.design.visitor.impl.Principal - 学生信息 姓名:windy 班级:一年一班
16:40:25.039 [main] INFO fun.lixj.design.visitor.impl.Principal - 学生信息 姓名:大毛 班级:二年三班
16:40:25.039 [main] INFO fun.lixj.design.visitor.impl.Principal - 学生信息 姓名:Shing 班级:三年四班
16:40:25.040 [main] INFO fun.lixj.design.visitor.impl.Principal - 学生信息 姓名:BK 班级:一年一班 升学率:99.77
16:40:25.040 [main] INFO fun.lixj.design.visitor.impl.Principal - 学生信息 姓名:娜娜Goddess 班级:一年一班 升学率:58.36
16:40:25.040 [main] INFO fun.lixj.design.visitor.impl.Principal - 学生信息 姓名:dangdang 班级:二年三班 升学率:33.56
16:40:25.040 [main] INFO fun.lixj.design.visitor.impl.Principal - 学生信息 姓名:泽东 班级:三年四班 升学率:2.04
Process finished with exit code 0
```
- 通过测试结果可以看到,家长和校长的访问视角同步,数据也是差异化的。
- 通过这样的测试结果,可以看到访问者模式的初心和结果,在适合的场景运用合适的模式,非常有利于程序开发。
# 总结
- 从以上的业务场景中可以看到,在嵌入访问者模式后,可以让整个工程结构变得容易添加和修改。也就做到了系统服务之间的解耦,不至于为了不同类型信息的访问而增加很多多余的 if 判断或者类的强制转换。也就是通过这样的设计模式而让代码结构更加清晰。
- 另外在实现的过程可能你可能也发现了,定义抽象类的时候还需要等待访问者接口的定义,这样的设计首先从实现上会让代码的组织变得有些难度。另外从设计模式原则的角度来看,违背了迪米特原则,也就是最少知道原则。因此在使用上一定要符合场景的运用,以及提取这部分设计思想的精髓。
- 好的学习方式才好更容易接受知识,学习编程的更需要的不单单是看,而是操作。二十多种设计模式每一种都有自己的设计技巧,也可以说是巧妙之处,这些巧妙的地方往往是解决复杂难题的最佳视角。亲力亲为,才能为所欲为,为了自己的欲望而努力!
>原文链接:https://github.com/fuzhengwei/CodeGuide/blob/master/docs/md/develop/design-pattern/2020-07-09-%E9%87%8D%E5%AD%A6%20Java%20%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E3%80%8A%E5%AE%9E%E6%88%98%E8%AE%BF%E9%97%AE%E8%80%85%E6%A8%A1%E5%BC%8F%E3%80%8B.md

【设计模式】访问者模式