一文说透访问者模式

点击蓝色 “陈树义” 关注我哟

作者:犀牛饲养员

来源:http://www.machengyu.net/tech/2019/11/11/visitor.html

01 什么是访问者模式

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

访问者模式 (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 实现来操作。

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 大致的工作流程是:

  1. ClassReader 读取字节码到内存中,生成用于表示该字节码的内部表示的树,ClassReader 对应于访问者模式中的元素。

  2. 组装 ClassVisitor 责任链,这一系列 ClassVisitor 完成了对字节码一系列不同的字节码修改工作,对应于访问者模式中的访问者 Visitor。

  3. 然后调用 ClassReader#accept () 方法,传入 ClassVisitor 对象,此 ClassVisitor 是责任链的头结点,经过责任链中每一个 ClassVisitor 的对已加载进内存的字节码的树结构上的每个节点的访问和修改。

  4. 最后,在责任链的末端,调用 ClassWriter 这个 visitor 进行修改后的字节码的输出工作。


推荐阅读

公众号 @陈树义,用最简单的语言,分享我的技术见解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值