点击蓝色 “陈树义” 关注我哟
作者:犀牛饲养员
来源:http://www.machengyu.net/tech/2019/11/11/visitor.html
01 什么是访问者模式
究竟什么是访问者模式呢?
访问者模式 (Visitor Pattern) 模式是行为型 (Behavioral) 设计模式,提供一个作用于某种对象结构上的各元素的操作方式,可以使我们在不改变元素结构的前提下,定义作用于元素的新操作。
如果系统的数据结构是比较稳定的,但其操作(算法)是易于变化的,那么使用访问者模式是个不错的选择;如果数据结构是易于变化的,则不适合使用访问者模式。
总结起来一句话:访问者模式的目的是将数据结构和数据操作分离,提供优秀的扩展性。
通常情况下,访问者模式里有以下几个角色:
Vistor(抽象访问者):为该对象结构中具体元素角色声明一个访问操作接口。
ConcreteVisitor(具体访问者):每个具体访问者都实现了 Vistor 中定义的操作。
Element(抽象元素):定义了一个 accept 操作,以 Visitor 作为参数。
ConcreteElement(具体元素):实现了 Element 中的 accept () 方法,调用具体的 Vistor 的访问方法以便完成对一个元素的操作。
ObjectStructure(对象结构):可以是组合模式,也可以是集合;能够枚举它包含的元素;提供一个接口,允许 Vistor 访问它的元素。
废话说了不少了,再不上源码就是耍流氓了。先来张图把源码的结构展示下:
这是一个电商的场景,我们有个抽象物品类 ItemElement 接口,两个具体的实现分别是书 Book,和水果 Fruit。接口通过 Accept 开放一个给访问者的入口。
public interface ItemElement {
int accept(ShoppingCartVisitor visitor);
}
@Data
@ToString
@AllArgsConstructor
class Book implements ItemElement {
private int price;
private String isbnNumber;
@Override
public int accept(ShoppingCartVisitor visitor) {
return visitor.visit(this);
}
}
@Data
@AllArgsConstructor
@ToString
class Fruit implements ItemElement
{
private int pricePerKg;
private int weight;
private String name;
@Override
public int accept(ShoppingCartVisitor visitor)
{
return visitor.visit(this);
}
}
接着定义一个 visitor 接口,里面有对具体的 element 的访问声明。而接口的实现类只要根据具体的 Element 实现类实现方法即可。
public interface ShoppingCartVisitor {
int visit(Book book);
int visit(Fruit fruit);
}
public class ShoppingCartVisitorImpl implements ShoppingCartVisitor {
@Override
public int visit(Book book) {
int cost=0;
if(book.getPrice() > 50) //折扣
{
cost = book.getPrice()-5;
}
else
cost = book.getPrice();
System.out.println("Book ISBN::"+book.getIsbnNumber() + " cost ="+cost);
return cost;
}
@Override
public int visit(Fruit fruit) {
int cost = fruit.getPricePerKg()*fruit.getWeight();
System.out.println(fruit.getName() + " cost = "+cost);
return cost;
}
}
然后是测试类
public class ShoppingCartClient {
public static void main(String[] args) {
ItemElement[] items = new ItemElement[]{new Book(20, "1234"),
new Book(100, "5678"), new Fruit(10, 2, "Banana"),
new Fruit(5, 5, "Apple")};
int total = calculatePrice(items);
System.out.println("Total Cost = "+total);
}
private static int calculatePrice(ItemElement[] items)
{
ShoppingCartVisitor visitor = new ShoppingCartVisitorImpl();
int sum=0;
for(ItemElement item : items)
{
sum = sum + item.accept(visitor);
}
return sum;
}
}
输出
Book ISBN::1234 cost =20
Book ISBN::5678 cost =95
Banana cost = 20
Apple cost = 25
Total Cost = 160
代码很简单,根据这个示例代码我们可以总结下实现访问者模式的基本步骤:
声明一个 visitor 接口,里面定义一组方法,每个方法是针对具体的 element 操作的定义。
声明一个 element 接口,里面定义一个 accept 方法,作为访问者访问的入口。
element 的访问操作都必须通过 visitor 接口来操作。
element 不同的行为模型可以通过具体的 visitor 实现来操作。
02 访问者模式的开源应用
前段时间看 ASM Java 字节码增强技术,里面就用到了访问者模式。访问者模式主要用于修改或操作一些数据结构比较稳定的数据,我们知道字节码文件的结构是由 JVM 固定的,所以很适合利用访问者模式对字节码文件进行修改。
ASM 中和访问者模式相关的有几个重要的类:ClassReader、ClassVisitor、ClassWriter。
ClassReader 相当于访问者模式中的 element,它将字节数组或者 class 文件读入到内存当中,并以树的数据结构表示,树中的一个节点代表着 class 文件中的某个区域。这个类定义了一个 accept 方法用来和 visitor 交互。
public class ClassReader {
public void accept(ClassVisitor classVisitor, int flags) {
this.accept(classVisitor, new Attribute[0], flags);
}
public void accept(ClassVisitor classVisitor, Attribute[] attrs, int flags) {
int u = this.header;
char[] c = new char[this.maxStringLength];
Context context = new Context();
context.attrs = attrs;
context.flags = flags;
context.buffer = c;
int access = this.readUnsignedShort(u);
String name = this.readClass(u + 2, c);
String superClass = this.readClass(u + 4, c);
String[] interfaces = new String[this.readUnsignedShort(u + 6)];
u += 8;
for(int i = 0; i < interfaces.length; ++i) {
interfaces[i] = this.readClass(u, c);
u += 2;
}
...
ClassVisitor 相当于抽象访问者接口。ClassReader 对象创建之后,调用 ClassReader#accept () 方法,传入一个 ClassVisitor 对象。在 ClassReader 中遍历树结构的不同节点时会调用 ClassVisitor 对象中不同的 visit () 方法,从而实现对字节码的修改。
public abstract class ClassVisitor {
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
if (this.cv != null) {
this.cv.visit(version, access, name, signature, superName, interfaces);
}
}
public void visitSource(String source, String debug) {
if (this.cv != null) {
this.cv.visitSource(source, debug);
}
}
public ModuleVisitor visitModule(String name, int access, String version) {
if (this.api < 393216) {
throw new RuntimeException();
} else {
return this.cv != null ? this.cv.visitModule(name, access, version) : null;
}
}
...
ClassWriter 是 ClassVisitor 的实现类,它负责把每一个节点修改后的字节码输出为字节数组。
public class ClassWriter extends ClassVisitor {
public final void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
this.version = version;
this.access = access;
this.name = this.newClass(name);
this.thisName = name;
if (signature != null) {
this.signature = this.newUTF8(signature);
}
this.superName = superName == null ? 0 : this.newClass(superName);
if (interfaces != null && interfaces.length > 0) {
this.interfaceCount = interfaces.length;
this.interfaces = new int[this.interfaceCount];
for(int i = 0; i < this.interfaceCount; ++i) {
this.interfaces[i] = this.newClass(interfaces[i]);
}
}
}
public final void visitSource(String file, String debug) {
if (file != null) {
this.sourceFile = this.newUTF8(file);
}
if (debug != null) {
this.sourceDebug = (new ByteVector()).encodeUTF8(debug, 0, 2147483647);
}
}
ASM 大致的工作流程是:
ClassReader 读取字节码到内存中,生成用于表示该字节码的内部表示的树,ClassReader 对应于访问者模式中的元素。
组装 ClassVisitor 责任链,这一系列 ClassVisitor 完成了对字节码一系列不同的字节码修改工作,对应于访问者模式中的访问者 Visitor。
然后调用 ClassReader#accept () 方法,传入 ClassVisitor 对象,此 ClassVisitor 是责任链的头结点,经过责任链中每一个 ClassVisitor 的对已加载进内存的字节码的树结构上的每个节点的访问和修改。
最后,在责任链的末端,调用 ClassWriter 这个 visitor 进行修改后的字节码的输出工作。
推荐阅读
公众号 @陈树义,用最简单的语言,分享我的技术见解。