1 简介
1.1 定义
封装某些作用于某种数据结构中各元素的操作,它可以在不改变数据结构的前提下定义作用于这些数据元素的新的操作。
1.2 思想
将数据结构和数据操作分离。
1.3 目的
稳定的数据结构和易变的操作的解耦。
1.4 适用场景
假如一个对象中存在着一些与本对象不相干(或者关系较弱)的操作,可以使用访问者模式把这些操作封装到访问者中去,这样便避免了这些不相干的操作污染这个对象。
假如一组对象中,存在着相似的操作,可以将这些相似的操作封装到访问者中去,这样便避免了出现大量重复的代码
访问者模式适用于对功能已经确定的项目进行重构的时候适用,因为功能已经确定,元素类的数据结构也基本不会变了;如果是一个新的正在开发中的项目,在访问者模式中,每一个元素类都有它对应的处理方法,每增加一个元素类都需要修改访问者类,修改起来相当麻烦。
结构
关键是如何将作用于元素的操作分离出来封装成独立的类。
主要角色
- 抽象访问者(Visitor)
定义一个访问具体元素的接口,为每个具体元素类对应一个访问操作 visit() ,该操作中的参数类型标识了被访问的具体元素。 - 具体访问者(ConcreteVisitor)
实现抽象访问者角色中声明的各个访问操作,确定访问者访问一个元素时该做什么。 - 抽象元素(Element)
声明一个包含接受操作 accept() 的接口,被接受的访问者对象作为 accept() 方法的参数。 - 具体元素(ConcreteElement)
实现抽象元素角色提供的 accept() 操作,其方法体通常都是 visitor.visit(this) ,另外具体元素中可能还包含本身业务逻辑的相关操作。 - 对象结构(Object Structure)
包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法,通常由 List、Set、Map 等聚合类实现。
UML

2 实例
- 老师教学反馈得分大于等于85分、学生成绩大于等于90分,则可以入选成绩优秀奖
- 老师论文数目大于8、学生论文数目大于2,则可以入选科研优秀奖。
老师和学生就是Element,他们的数据结构稳定不变。
但对数据结构的操作是多变的,一会儿评选成绩,一会儿评选科研,这就适合使用访问者模式分离【数据结构】和【操作】。
2.1 创建抽象元素
public interface Element {
void accept(Visitor visitor);
}2.2 创建具体元素
创建两个具体元素 Student 和 Teacher,分别实现 Element 接口
public class Student implements Element {
private String name;
private int grade;
private int paperCount;
public Student(String name, int grade, int paperCount) {
= name;
this.grade = grade;
this.paperCount = paperCount;
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
......
}
public class Teacher implements Element {
private String name;
private int score;
private int paperCount;
public Teacher(String name, int score, int paperCount) {
= name;
this.score = score;
this.paperCount = paperCount;
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
...
}2.3 创建抽象访问者
2.4 创建具体访问者
创建一个根据分数评比的具体访问者 GradeSelection,实现 Visitor 接口
public class GradeSelection implements Visitor {
@Override
public void visit(Student student) {
if (student != null && student.getGrade() >= 90) {
System.out.println(student.getName() + "的分数是" + student.getGrade() + ",荣获了成绩优秀奖。");
}
}
@Override
public void visit(Teacher teacher) {
if (teacher != null && teacher.getScore() >= 85) {
System.out.println(teacher.getName() + "的分数是" + teacher.getScore() + ",荣获了成绩优秀奖。");
}
}
}2.5 调用
public class VisitorClient {
public static void main(String[] args) {
// 抽象元素 => 具体元素
Element element = new Student("lijiankun24", 90, 3);
// 抽象访问者 => 具体访问者
Visitor visitor = new GradeSelection();
// 具体元素 接收 具体访问者的访问
element.accept(visitor);
}
}上述代码可以分为三步:
- 创建一个元素类的对象
- 创建一个访问类的对象
- 元素对象通过 Element#accept(Visitor visitor) 传入访问者对象
3 ASM 中的访问者模式
ASM 库就是 Visitor 模式的典型应用。
3.1 ASM 中几个重要的类
ClassReader
将字节数组或者 class 文件读入到内存当中,并以树的数据结构表示,树中的一个节点代表着 class 文件中的某个区域
可以将 ClassReader 看作是 Visitor 模式中的访问者的实现类
ClassVisitor(抽象类)
ClassReader 对象创建之后,调用 ClassReader#accept() 方法,传入一个 ClassVisitor 对象。在 ClassReader 中遍历树结构的不同节点时会调用 ClassVisitor 对象中不同的 visit() 方法,从而实现对字节码的修改。在 ClassVisitor 中的一些访问会产生子过程,比如 visitMethod 会产生 MethodVisitor 的调用,visitField 会产生对 FieldVisitor 的调用,用户也可以对这些 Visitor 进行自己的实现,从而达到对这些子节点的字节码的访问和修改。
在 ASM 的访问者模式中,用户还可以提供多种不同操作的 ClassVisitor 的实现,并以责任链的模式提供给 ClassReader 来使用,而 ClassReader 只需要 accept 责任链中的头节点处的 ClassVisitor。
ClassWriter
ClassVisitor 的实现类,它是生成字节码的工具类,它一般是责任链中的最后一个节点,其之前的每一个 ClassVisitor 都是致力于对原始字节码做修改,而 ClassWriter 的操作则是老实得把每一个节点修改后的字节码输出为字节数组。
3.2 ASM 的工作流程
- ClassReader 读取字节码到内存中,生成用于表示该字节码的内部表示的树,ClassReader 对应于访问者模式中的元素
- 组装 ClassVisitor 责任链,这一系列 ClassVisitor 完成了对字节码一系列不同的字节码修改工作,对应于访问者模式中的访问者 Visitor
- 然后调用 ClassReader#accept() 方法,传入 ClassVisitor 对象,此 ClassVisitor 是责任链的头结点,经过责任链中每一个 ClassVisitor 的对已加载进内存的字节码的树结构上的每个节点的访问和修改
- 最后,在责任链的末端,调用 ClassWriter 这个 visitor 进行修改后的字节码的输出工作
优点
- 扩展性好
能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。 - 复用性好
可通过访问者来定义整个对象结构通用的功能,从而提高系统的复用程度。 - 灵活性好
访问者模式将数据结构与作用于结构上的操作解耦,使得操作集合可相对自由地演化而不影响系统的数据结构。 - 符合单一职责原则
访问者模式把相关的行为封装在一起,构成一个访问者,使每一个访问者的功能都比较单一。
缺点
- 增加新的元素类很困难
在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。 - 破坏封装
访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。 - 违反了依赖倒置原则
访问者模式依赖了具体类,而没有依赖抽象类。
















