一文说透访问者模式

2019/11/11

基本介绍

究竟什么是访问者模式呢?

访问者模式(Visitor Pattern)模式是行为型(Behavioral)设计模式,提供一个作用于某种对象结构上的各元素的操作方式,可以使我们在不改变元素结构的前提下,定义作用于元素的新操作。

如果系统的数据结构是比较稳定的,但其操作(算法)是易于变化的,那么使用访问者模式是个不错的选择;如果数据结构是易于变化的,则不适合使用访问者模式。

总结起来一句话,

访问者模式的目的是将数据结构和数据操作分离,提供优秀的扩展性。

通常情况下,访问者模式里有以下几个角色:

  1. Vistor(抽象访问者):为该对象结构中具体元素角色声明一个访问操作接口。

  2. ConcreteVisitor(具体访问者):每个具体访问者都实现了Vistor中定义的操作。

  3. Element(抽象元素):定义了一个accept操作,以Visitor作为参数。

  4. ConcreteElement(具体元素):实现了Element中的accept()方法,调用具体的Vistor的访问方法以便完成对一个元素的操作。

  5. 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

代码很简单,根据这个示例代码我们可以总结下实现访问者模式的基本步骤:

  1. 声明一个visitor接口,里面定义一组方法,每个方法是针对具体的element操作的定义。
  2. 声明一个element接口,里面定义一个accept方法,作为访问者访问的入口。
  3. element的访问操作都必须通过visitor接口来操作。
  4. element不同的行为模型可以通过具体的visitor实现来操作。

使用访问者模式的一些源码分析

前段时间看ASM Java字节码增强技术,里面就用到了访问者模式。访问者模式主要用于修改或操作一些数据结构比较稳定的数据,我们知道字节码文件的结构是由JVM固定的,所以很适合利用访问者模式对字节码文件进行修改。

ASM中和访问者模式相关的有几个重要的类,

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

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 进行修改后的字节码的输出工作

参考:

  • https://www.geeksforgeeks.org/visitor-design-pattern/
  • https://www.jianshu.com/p/e4b8cb0b3204


欢迎关注我的公众号

(转载本站文章请注明作者和出处 犀牛饲养员

Show Disqus Comments

Post Directory