【设计模式】【行为型模式】访问者模式(Visitor)
👋hi,我不是一名外包公司的员工,也不会偷吃茶水间的零食,我的梦想是能写高端CRUD
🔥 2025本人正在沉淀中… 博客更新速度++
👍 欢迎点赞、收藏、关注,跟上我的更新节奏
🎵 当你的天空突然下了大雨,那是我在为你炸乌云
文章目录
- 一、入门
- 什么是访问者模式?
- 为什么需要访问者模式?
- 怎么实现访问模式?
- 二、访问者模式在源码中的运用
- ASM 框架(Java 字节码操作)
- 三、总结
- 访问者模式的优点
- 访问者模式的缺点
- 访问者模式的适用场景
一、入门
什么是访问者模式?
访问者模式(Visitor Pattern)是一种行为设计模式,允许你将算法与对象结构分离。通过这种方式,可以在不改变对象结构的情况下,向对象结构中的元素添加新的操作。
为什么需要访问者模式?
假设有一个图形对象结构,包含Circle
和Rectangle
两种元素,需要实现两种操作:计算面积和导出为JSON。
传统实现(无访问者模式):
// 元素类
interface Shape {
double calculateArea(); // 操作1:计算面积
String toJson(); // 操作2:导出为JSON
}
class Circle implements Shape {
@Override
public double calculateArea() { /* 实现 */ }
@Override
public String toJson() { /* 实现 */ }
}
class Rectangle implements Shape {
@Override
public double calculateArea() { /* 实现 */ }
@Override
public String toJson() { /* 实现 */ }
}
问题
- 违反开闭原则
- 当需要为对象结构添加新操作时(例如计算、导出、校验等),必须修改每个元素类的代码。
- 示例:每新增一个操作(如导出为XML),所有
Shape
子类都需要修改。
- 代码冗余和分散
- 相关操作分散在各个元素类中,难以集中管理。
- 示例:如果“校验”逻辑分散在
Circle.validate()
和Rectangle.validate()
中,维护和扩展会变得困难。
- 难以扩展复杂操作
- 某些操作需要跨多个元素协作(例如统计图形的面积总合),直接写在元素类中会导致职责混乱。
怎么实现访问模式?
访问者模式的构成如下:
- Visitor(访问者):定义了对每个元素(Element)的访问操作,通常为每个具体元素类提供一个访问方法。
- ConcreteVisitor(具体访问者):实现Visitor接口,定义具体的操作。
- Element(元素):定义一个接受访问者的方法(accept),通常是一个接口或抽象类。
- ConcreteElement(具体元素):实现Element接口,提供具体的accept方法实现。
- ObjectStructure(对象结构):包含一组元素,通常提供一个方法让访问者访问其中的所有元素。
【案例】图形对象结构 - 改
Visitor(访问者接口):对应 ShapeVisitor
接口
作用:定义访问操作的接口,声明对不同具体元素(如Circle
、Rectangle
)的访问方法。
interface ShapeVisitor {
void visit(Circle circle); // 访问 Circle 元素
void visit(Rectangle rectangle); // 访问 Rectangle 元素
}
ConcreteVisitor(具体访问者):对应AreaCalculator
和JsonExporter
类
作用:实现 ShapeVisitor
接口,定义具体的操作逻辑(如计算面积、导出JSON)。
// 具体访问者1:计算面积
class AreaCalculator implements ShapeVisitor {
@Override
public void visit(Circle circle) { /* 计算圆的面积 */ }
@Override
public void visit(Rectangle rectangle) { /* 计算矩形的面积 */ }
}
// 具体访问者2:导出为JSON
class JsonExporter implements ShapeVisitor {
@Override
public void visit(Circle circle) { /* 导出圆的JSON */ }
@Override
public void visit(Rectangle rectangle) { /* 导出矩形的JSON */ }
}
Element(元素接口):对应Shape
接口
作用:定义元素的通用行为,即通过accept
方法接受访问者。
interface Shape {
void accept(ShapeVisitor visitor); // 接受访问者的入口
}
ConcreteElement(具体元素):对应Circle
和Rectangle
类
作用:实现Shape
接口,在accept
方法中将自身传递给访问者的具体方法(如visit(Circle)
)。
class Circle implements Shape {
@Override
public void accept(ShapeVisitor visitor) {
visitor.visit(this); // 调用访问者的 visit(Circle) 方法
}
}
class Rectangle implements Shape {
@Override
public void accept(ShapeVisitor visitor) {
visitor.visit(this); // 调用访问者的 visit(Rectangle) 方法
}
}
ObjectStructure(对象结构):通常是一个管理元素集合的类。
作用:负责维护一组元素(如Shape
对象),并提供遍历方法让访问者访问所有元素。
class ShapeCollection {
private List<Shape> shapes = new ArrayList<>();
public void addShape(Shape shape) {
shapes.add(shape);
}
// 让访问者遍历所有元素
public void accept(ShapeVisitor visitor) {
for (Shape shape : shapes) {
shape.accept(visitor);
}
}
}
二、访问者模式在源码中的运用
ASM 框架(Java 字节码操作)
ASM 框架是一个用于操作 Java 字节码的库,广泛用于动态生成类、修改类文件(如 AOP、代码增强)等场景。它通过访问者模式(Visitor Pattern)将字节码的解析和生成与具体操作解耦。
ASM 通过以下步骤实现字节码操作:
- 解析字节码:
ClassReader
读取 .class 文件。 - 触发访问者:
ClassReader
将字节码中的每个元素(类、方法、字段等)传递给ClassVisitr
。 - 处理元素:开发者通过自定义的
ClassVisitor
和MethodVisitor
修改或分析字节码。 - 生成新字节码:
ClassWriter
将修改后的字节码写入新的 .class 文件。
访问者模式角色 ASM 中的实现:
- Visitor:
ClassVisitor
、MethodVisitor
、FieldVisitor
等接口 - ConcreteVisitor: 开发者自定义的
ClassVisitor
、MethodVisitor
实现 - Element: 字节码中的结构(类、方法、字段、指令等)
- ConcreteElement: 具体的类、方法、字段(如
visitMethod
中的方法描述符) - ObjectStructure:
ClassReader
(负责遍历类文件并触发访问者方法)
Visitor: 以ClassVisitor
为例子,它的作用是,定义访问类结构的操作,例如访问类头、方法、字段等。
public abstract class ClassVisitor {
protected final int api;
protected ClassVisitor cv;
...
public void visit(
final int version,
final int access,
final String name,
final String signature,
final String superName,
final String[] interfaces) {
if (api < Opcodes.ASM8 && (access & Opcodes.ACC_RECORD) != 0) {
throw new UnsupportedOperationException("Records requires ASM8");
}
if (cv != null) {
cv.visit(version, access, name, signature, superName, interfaces);
}
}
...
ConcreteVisitor:我们用户自己实现的Visitor接口的类
public class TimerClassVisitor extends ClassVisitor {
public TimerClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM9, cv);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
// 为所有非构造方法添加计时逻辑
if (!name.equals("<init>")) {
mv = new MethodTimerVisitor(mv, name);
}
return mv;
}
}
Element:在 ASM 框架中,Element
的概念是隐式的,而不是显式地通过一个接口或类来表示。字节码中的各种结构(如类、方法、字段、指令等)可以被视为Element
,但它们并没有一个统一的接口或基类。相反,ASM 通过访问者模式直接操作这些结构。
在 ASM 中,以下结构可以被视为Element
:
- 类:通过
ClassVisitor.visit
方法的参数(如类名、父类、接口等)表示。 - 方法:通过
ClassVisitor.visitMethod
方法的参数(如方法名、描述符等)表示。 - 字段:通过
ClassVisitor.visitField
方法的参数(如字段名、类型等)表示。 - 指令:通过
MethodVisitor.visitInsn
、visitMethodInsn
等方法的参数表示。
ConcreteElement :在 ASM 框架中,ConcreteElement
并不是通过一个显式的类或接口来表示的,而是通过访问者模式的方法参数隐式传递的。具体来说,字节码中的各种结构(如类、方法、字段、指令等)可以被视为ConcreteElement
,但它们并没有一个统一的基类或接口。
在ClassVisitor.visit
方法中,类的信息通过参数传递:
void visit(
int version, // 类版本
int access, // 访问标志(如 public、final)
String name, // 类名
String signature, // 泛型签名
String superName, // 父类名
String[] interfaces // 实现的接口
);
ObjectStructure:在 ASM 框架中,ClassReader
就是这个角色,它负责读取类文件并触发访问者模式的方法调用。
以下是accept
方法的简化伪代码,展示了ClassReader
如何触发访问者方法:
public void accept(ClassVisitor cv, int parsingOptions) {
// 解析类头
cv.visit(version, access, name, signature, superName, interfaces);
// 解析字段
for (FieldInfo field : fields) {
cv.visitField(field.access, field.name, field.desc, field.signature, field.value);
}
// 解析方法
for (MethodInfo method : methods) {
MethodVisitor mv = cv.visitMethod(method.access, method.name, method.desc, method.signature, method.exceptions);
if (mv != null) {
// 解析方法体
for (Instruction insn : method.instructions) {
mv.visitInsn(insn.opcode);
}
mv.visitEnd();
}
}
// 结束访问
cv.visitEnd();
}
三、总结
访问者模式的优点
- 开闭原则
- 优点:新增操作时只需添加新的访问者类,无需修改现有的对象结构。
- 示例:如果需要为类结构添加新的操作(如导出为XML),只需实现一个新的访问者类。
- 单一职责原则
- 优点:将相关操作集中在一个访问者类中,便于维护和扩展。
- 示例:将“计算面积”和“导出为JSON”的逻辑分别放在不同的访问者类中。
- 灵活性
- 优点:可以在不修改对象结构的情况下,动态地为对象结构添加新的操作。
- 示例:在编译器中使用访问者模式实现语法树的遍历和优化。
- 解耦数据结构与操作
- 优点:将数据结构与操作逻辑分离,使得代码更清晰、更易于维护。
- 示例:ASM 框架通过访问者模式将字节码解析与操作逻辑解耦。
访问者模式的缺点
- 增加新元素类型困难
- 缺点:每增加一种新元素类型,所有访问者类都需要修改。
- 示例:如果在类结构中新增一种元素(如注解),所有访问者类都需要添加对应的
visit
方法。
- 破坏封装
- 缺点:访问者可能需要访问元素的私有成员,从而破坏封装性。
- 示例:访问者可能需要直接访问类的私有字段来完成某些操作。
- 复杂性增加
- 缺点:访问者模式引入了额外的类和接口,增加了代码的复杂性。
- 示例:需要定义
Visitor
接口、ConcreteVisitor
实现类以及Element
接口。
- 性能开销
- 缺点:双重分派机制可能带来一定的性能开销。
- 示例:在性能敏感的场景中,访问者模式可能不如直接操作对象结构高效。
访问者模式的适用场景
- 对象结构稳定,但操作频繁变化
- 场景:对象结构很少变化,但需要频繁添加新的操作。
- 示例:编译器中的语法树遍历(如代码优化、静态分析)。
- 需要对对象结构进行多种不相关的操作
- 场景:对象结构需要支持多种不相关的操作,且希望将这些操作集中管理。
- 示例:文档对象模型(DOM)的遍历和操作(如渲染、导出、校验)。
- 操作需要跨多个元素协作
- 场景:某些操作需要访问多个元素并协作完成。
- 示例:统计文档中所有图片的尺寸总和。
- 避免污染元素类的代码
- 场景:希望保持元素类的简洁,避免将操作逻辑分散在各个元素类中。
- 示例:ASM 框架通过访问者模式避免将字节码操作逻辑分散在类、方法、字段等元素中。