ProGuard 进阶系列(四)访问者模式

news2024/11/17 5:40:39

在进行后面的内容分析之前,不得不讲到访问者模式,这是 GOF 23 个设计模式中最难的几个模式之一。如果能够很好的理解访问者模式, 后续源码解读会相对容易一些。本文将结合 ProGuard 的部分源码,理解分析访问者模式的用途及使用场景。

一、什么是访问者模式

先来看定义,访问者模式是一种将对象操作算法与对象结构分离的设计模式。这句话很抽象,不是很好理解。用通俗的话来讲就是:我们在写代码的时候,一般情况下,会将对象的操作算法定义此对象内部,直接通过对象的某个方法去获取信息或进行对象操作,当有新需求变更时,就需要对此对象的代码进行修改,来满足新的需求。但在访问者模式中,会将对象结构固定下来,并且后续不在对其修改。面对新的需求时,不需要去修改对象结构,通过添加新的访问者去实现对象操作。访问者模式遵从「开闭原则」。当然,并不是所有场景都适合使用访问者模式,一般使用访问者模式的场景如下:

  1. 对象结构比较稳定,但经常需要在此对象结构上定义新的操作。 如 Class 文件,它的结构基本不会修改。

  2. 需要对一个对象结构中的对象进行很多不同且不相关的操作,为避免这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。 如读取 Class 文件中的方法、属性等信息。

在来看一下访问者模式的类图:

fba1c2fedf93d0d92495e4b06c1a4ba0.png
访问者模式类图

介绍一下,类图中最重要的两个角色:

  • Visitor:接口或者抽象类,定义对 Element 的访问行为,visit 方法的参数为要访问的 Element。理论上,它的方法数与要访问的 Element 个数一致。因此,访问者模式要求的类型要稳定,如果经常添加、移除 Element,必然会导致频繁地修改 Visitor 接口,如果出现这种情况,则说明不适合使用访问者模式。

  • Element:元素接口或者抽象类,它定义了一个接受访问者(accept)的方法,其意义是指每一个元素都要可以被访问者访问。

看到这里,依然很抽象,但是我相信应该有一个大体的了解,为了更好的理解这个模式,下面将从 ProGuard中对 class 文件的部分操作,还原访问者模式的诞生过程。

二、访问者模式的诞生过程

要想更好的理解访问者模式,还是得从真实案例入手。在 ProGuard 中, 主要操作的是 Class 文件,而 Class 文件的格式非常固定,Oracle 文档中,对 Class 文件定义如下:

832b01636edae61ece78ffb407bc4058.png
ClassFile 的定义

并且在 ProGuard 的执行过程中,可能会源源不断的增加对 ClassFile 的操作。 这就很好的对应上了使用访问者模式的两个条件。

先来思考一下,在 ProGuard 中,对 Class 文件的操作有哪些?

  1. 从文件中读取 Class 的内容,并填充到  ProgramClass  对象中

  2. ProgramClass 对象进行修改,如修改类名、变量名、方法名、方法参数类型等

  3. ProgramClass 对象的信息写入到 Class 文件

在这里,看一下从文件中读取 Class 的内容,根据定义,在 ClassFile 中,会有一个常量池,常量池中可能会有各种不同的常量, 如 IntegerConstantFloatConstantStringConstantUtf8Constant 等。现在要实现从 Class 文件中,读取这些信息出来。如果让你来实现,你会怎么做呢?

实现这个功能并不难,不同的人有不同的写法,我将其中一种实现方式代码贴在下面。其中,Constant 是一个接口,包含一个方法 readFromResource

abstract class Constant {
    abstract void readFromResource(DataInput dataInput) throws Exception;
}

class IntegerConstant extends Constant {
    public int u4value;

    @Override
    void readFromResource(DataInput dataInput) throws Exception {
        this.u4value = dataInput.readInt();
    }
}

class Utf8Constant extends Constant {
    public byte[] bytes;
    @Override
    void readFromResource(DataInput dataInput) throws Exception {
        int u2length = dataInput.readUnsignedShort();
        System.out.println(u2length);
        byte[] bytes = new byte[u2length];
        dataInput.readFully(bytes);
        this.bytes = bytes;
    }
}

public class VisitorV1 {
    public static void main(String[] args) throws Exception {
        File clazzFile = new File("visit.class");
        DataInput dataInput = new DataInputStream(new FileInputStream(clazzFile));
        for (Constant constant : listAllConstant()) {
            constant.readFromResource(dataInput);
            System.out.println(constant);
        }
    }
    public static List<Constant> listAllConstant() {
        List<Constant> list = new ArrayList<>();
        list.add(new IntegerConstant());
        list.add(new Utf8Constant());
        return list;
    }
}

针对这些 Constant ,我们不仅需要读取,还需要修改、写文件等逻辑。如果继续按照这个思路写下去,我们需要在添加 modifysave  等方法,这将带来一些新的问题:

  • 违背开闭原则,添加新功能,所有类的代码都需要进行更改。

  • 随着功能增多,每个类的代码也会不断膨胀,可读性和可维护性都会变差

  • 上层的业务逻辑与具体的类耦合在一起,会导致类的职责不单一

要解决这个问题,常用的解决方法就是拆分解藕,把业务操作与数据结构进行解藕,设计成独立的类。这里我们按照访问者模式的演进思路对代码进行重构,重构后代码如下:

abstract class Constant {
}

class IntegerConstant extends Constant {
    public int u4value;
}

class Utf8Constant extends Constant {
    public byte[] bytes;
}


class Reader {
    public DataInput dataInput;

    public Reader(DataInput dataInput) {
        this.dataInput = dataInput;
    }

    public void readFromResource(IntegerConstant constant) throws Exception {
        constant.u4value = dataInput.readInt();
    }

    public void readFromResource(Utf8Constant constant) throws Exception {
        int u2length = dataInput.readUnsignedShort();
        byte[] bytes = new byte[u2length];
        dataInput.readFully(bytes);
        constant.bytes = bytes;
    }
}

public class VisitorV2 {
    public static void main(String[] args) throws Exception {
        File clazzFile = new File("visit.class");
        DataInput dataInput = new DataInputStream(new FileInputStream(clazzFile));
        Reader reader = new Reader(dataInput);
        for (Constant constant : listAllConstant()) {
            reader.readFromResource(constant);
            System.out.println(constant);
        }
    }

    public static List<Constant> listAllConstant() {
        List<Constant> list = new ArrayList<>();
        list.add(new IntegerConstant());
        list.add(new Utf8Constant());
        return list;
    }
}

这其中最关键的一点设计是, 我们将读取不同类型常量的操作,设计成了三个重载函数。众所周知,重载函数是指在同一个类中函数名相同、参数不同的一组函数。

针对重构后的代码,如果你足够细心,就会发现上面的代码,其实是无法编译通过。

reader.readFromResource(constant);

此行代码编译会报错,报错如下:

99db8a7e77777688e84684f7fcfa27ca.png
编译报错信息

这是为什么呢?

要解释这个问题,先说一下 Java 中的多态,它是一种动态绑定,在运行时可以获取对象的具体类型,然后在运行其实际类型对应的方法。我们就是希望代码在运行的时候,能够像多态那样,在运行时能够根据参数类型去调不同的 readFromResource 方法。然而在代码实现中使用的是函数重载,它是一种静态绑定。代码在编译时并不能识别对象的具体类型,因此上面代码编译时就会出现「找不到合适的方法」这个错误。

解决办法就是访问者模式的精髓,先来看一下代码:

abstract class Constant {
    public abstract void accept(Reader reader) throws Exception;
}

class IntegerConstant extends Constant {
    public int u4value;

    @Override
    public void accept(Reader reader) throws Exception {
        reader.readFromResource(this);
    }
}

class Utf8Constant extends Constant {
    public byte[] bytes;

    @Override
    public void accept(Reader reader) throws Exception {
        reader.readFromResource(this);
    }
}

// 与前面 Reader 一样

public class VisitorV3 {
    public static void main(String[] args) throws Exception {
        File clazzFile = new File("visit.class");
        DataInput dataInput = new DataInputStream(new FileInputStream(clazzFile));
        Reader reader = new Reader(dataInput);
        for (Constant constant : listAllConstant()) {
            constant.accept(reader);
            System.out.println(constant);
        }
    }
}

在执行 constant.accept(reader) 的时候,根据  Java 的多态性,在运行时,就会调用实际类型的 accept 函数, 比如 Utf8Constantaccept 函数,而此时在 Utf8Constant 中会调用 ReaderreadFromResource 方法,此处的参数 this 类型就是 Utf8Constant ,在编译时就确定了,所以会调用 readFromResource(Utf8Constant constant) 这个重载函数。

如果,你看懂了这个,就基本已经理解了访问者模式。现在,我们继续添加新的功能, 例如我需要对 Constant  进行修改或重新写入新的文件。此时,实现可以参考前面的 Reader ,实现新的 ModifierWriter,并且给这两个类分别定义二个重载方法,以实现对不同 Constant 进行操作。当然,还需要在 Constant 中添加新的 accept 方法,具体实现如下:

abstract class Constant {
    public abstract void accept(Reader reader) throws Exception;
    public void accept(Writer writer) throws Exception;
}

class IntegerConstant extends Constant {
    public int u4value;

    @Override
    public void accept(Reader reader) throws Exception {
        reader.readFromResource(this);
    }
    @Override
    public void accept(Writer writer) throws Exception {
        writer.writeToResource(this);
    }
}
// ... 省略部分代码 

class Writer {
    public DataOutput dataOutput;

    public Writer(DataOutput dataOutput) {
        this.dataOutput = dataOutput;
    }

    public void writeToResource(IntegerConstant constant) throws Exception {
                dataOutput.writeInt(constant.u4value);
    }

    public void readFromResource(Utf8Constant constant) throws Exception {
          dataOutput.writeShort(constant.bytes.length);
                dataOutput.write(constant.bytes);
    }
}
// ... 省略测试入口

不同的操作已经抽象到不同的实现类中去了,但是每一次添加新的功能,都需要去修改对应的资源文件,不仅工作量巨大,而且违反了开闭原则。针对这个问题,可以回到文件的那个类图,我们可以设计一个 Visitor 的接口,并在接口中定义访问这些不同常量的 visit 方法,按照这个思路,重构后的代码示例如下:

abstract class Constant {
    public abstract void accept(Visitor visitor) throws Exception;
}

class IntegerConstant extends Constant {
    public int u4value;

    @Override
    public void accept(Visitor visitor) throws Exception {
        visitor.visit(this);
    }
}

class Utf8Constant extends Constant {
    public byte[] bytes;

    @Override
    public void accept(Visitor visitor) throws Exception {
        visitor.visit(this);
    }
}

interface Visitor {
    void visit(IntegerConstant constant) throws Exception;

    void visit(Utf8Constant constant) throws Exception;
}

class Reader implements Visitor {
    public DataInput dataInput;

    public Reader(DataInput dataInput) {
        this.dataInput = dataInput;
    }

    public void visit(IntegerConstant constant) throws Exception {
        constant.u4value = dataInput.readInt();
    }

    public void visit(Utf8Constant constant) throws Exception {
        int u2length = dataInput.readUnsignedShort();
        byte[] bytes = new byte[u2length];
        dataInput.readFully(bytes);
        constant.bytes = bytes;
    }
}

class Writer implements Visitor {

    public DataOutput dataOutput;

    public Writer(DataOutput dataOutput) {
        this.dataOutput = dataOutput;
    }

    @Override
    public void visit(IntegerConstant constant) throws Exception {
        dataOutput.writeInt(constant.u4value);
    }

    @Override
    public void visit(Utf8Constant constant) throws Exception {
        dataOutput.writeShort(constant.bytes.length);
        dataOutput.write(constant.bytes);
    }
}

三、总结

从上面例子中代码迭代中,应该能够很好地理解访问者模式了。访问者模式在日常开发工作中,很少会用到。访问者模式实现了一个或多个操作应用到一组对象上,设计意图是解耦操作与对象本身,保持类职责单一,满足开闭原则以应对直接修改代码带来的复杂性。

因为访问者模式实现非常的不好理解,在项目中使用此模式会导致代码的可读性降低。所以在项目中是否需要使用此模式,需要谨慎评估。

最后,ASM 以及 ProGuard 中对 Class 文件操作都很好地应用了访问者模式,有兴趣可以深度理解一下其源码实现,相信有不一样的收获。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/711741.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

单相逆变原理验证仿真

为了用全桥控制一个电感中的电流跟踪指令电流,simulink图如下图1&#xff1a; 图1 前向通道传递函数&#xff0c;闭环伯德图如下&#xff1a; s tf(s); %LC及并联电阻R的值 L1*10^-3; R10; %调制限号到占空比的传递函数&#xff0c;dVcont/Vramp -> d/Vcont 1/Vramp1Kcd …

JavaWeb 笔记——1

JavaWeb 笔记-1 初始JavaWeb什么是JavaWeb 一、JDBC1.1、JDBC简介1.2、API详解-DriverManager1.3、API详解-Connection1.4、API详解-Statement1.5、API详解-ResultSet1.6、API详解-PreparedStatement1.6.1、API详解-PreparedStatement-SQL注入演示1.6.2、API详解-PreparedState…

NR PDCP(二) format/parameters

这篇主要是PDCP PDU&#xff0c;PDCP Formats及其参数的含义&#xff0c; 收发过程中用到的变量&#xff0c;常量和定时器的总结&#xff0c;都是概念性描述。 PDU 和RLC 类似&#xff0c;PDCP PDU 也分为data pdu 和control PDU。PDCP data PDU 的data field对应的是user pla…

FPGA USB FX2 ov5640摄像头视频采集 驱动CY7C68013A实现 提供2套工程源码和技术支持

目录 1、前言2、我这儿已有的 FPGA USB 通信方案3、CY7C68013A芯片解读和硬件设计FX2 简介SlaveFIFO模式及其配置 4、工程详细设计方案5、vivado工程6、上板调试验证7、福利&#xff1a;工程代码的获取 1、前言 目前USB2.0的实现方案很多&#xff0c;但就简单好用的角度而言&a…

SpringBoot的缓存管理

缓存是分布式系统中的重要组件&#xff0c;主要解决数据库数据的高并发访问问题。在实际开发中&#xff0c;尤其是用户 访问量较大的网站&#xff0c;为了提高服务器访问性能、减少数据库的访问压力、提高用户体验&#xff0c;使用缓存显得 尤为重要。Spring Boot对缓存提供了良…

js将后台返回的数据转化为树形结构(扁平数组转树状结构)

前言 做项目使常遇到需要将后台返回的数据&#xff0c;转换为树状结构给用户展现&#xff0c;例如&#xff1a; 这也是前端面试常考的算法题&#xff0c;一起来检测一下吧。 步骤 准备一个空的树对象。遍历列表中的每个元素。对于每个元素&#xff0c;根据该元素的父级ID找到…

Linux常用命令——fdisk命令

在线Linux命令查询工具 fdisk 查看磁盘使用情况和磁盘分区 补充说明 fdisk命令用于观察硬盘实体使用情况&#xff0c;也可对硬盘分区。它采用传统的问答式界面&#xff0c;而非类似DOS fdisk的cfdisk互动式操作界面&#xff0c;因此在使用上较为不便&#xff0c;但功能却丝…

ERP重构-SLA子分类账-分布式实现方案

背景 ERP中的GL总账模块&#xff0c;明细数据来源于各个业务模块如库存、成本、应收、应付、费控、资产等&#xff0c;统称为子模块&#xff0c;生成的账叫做子分类账。然而记账的业务逻辑各式各样&#xff0c;但是最终输出都是来源、类型、期间、科目、借贷金额等等关键信息。…

Java-数据结构(二)-Map:HashMap、TreeMap、LinkedHashMap

目录 一、 引言二、问题2.1 什么是Map2.2 使用Map的好处2.3 Map的底层原理2.4 Key和Value的含义2.5 Key值为什么不能重复2.6 Key值和Hash的关系 三、 HashMap3.1 初始化HashMap3.2 添加和获取元素3.3 遍历HashMap3.4 删除元素3.5实现原理①HashMap的put()方法②HashMap的get()方…

【海量数据挖掘/数据分析】 之 贝叶斯分类算法(朴素贝叶斯分类、贝叶斯分类计算流程、拉普拉斯修正、贝叶斯分类实例计算)

【海量数据挖掘/数据分析】 之 贝叶斯分类算法&#xff08;朴素贝叶斯分类、贝叶斯分类计算流程、拉普拉斯修正、贝叶斯分类实例计算&#xff09; 目录 【海量数据挖掘/数据分析】 之 贝叶斯分类算法&#xff08;朴素贝叶斯分类、贝叶斯分类计算流程、拉普拉斯修正、贝叶斯分类…

无java环境运行jar

1、编写简单java程序。 例&#xff1a; public static void main(String[] args) {if(args.length>0)System.out.println("Params is&#xff1a;"args[0]);System.out.println("Hello word ! I am demo&#xff0c;&#xff0c;&#xff0c;&#xff0c…

软考高级网规考试笔记(涉及表格用图片代替_9万字左右)

作者&#xff1a;BSXY_19计科_陈永跃_23年7月更 BSXY_信息学院_v:CwJp0403 注&#xff1a;未经允许禁止转发任何内容 笔记说明&#xff1a; 目前还只有笔记&#xff0c;其他资源将会在近期更新&#xff0c;&#xff08;笔记5.5即可&#xff0c;不诚勿加可以去其他地方自找找看&…

Quartz任务调度笔记

一、概念 1.1简介 Quzrtz是OpenSymphony开源组织在Job scheduling领域的开源项目 特点&#xff1a;Quartz具有丰富特性的"任务调度库"&#xff0c;能够集成于任何的Java应用&#xff0c;小到独立的应用&#xff0c;大到电子商业系统。quartz能够创建亦简单亦复杂的调…

DAY35:贪心算法(二)分发饼干+摆动序列

文章目录 455.分发饼干思路两个for循环嵌套的写法为什么这种写法必须要有visited数组debug测试逻辑问题&#xff1a;没有进行计数逻辑问题&#xff1a;找到了result3个孩子 一层for循环的写法为什么这种写法一定要把小孩数组放在外面 376.摆动序列&#xff08;逻辑问题&#xf…

02_04实时调度类及SMP多核处理器的实时操作系统体系结构

上一篇文章说的是普通进程的调度但同时还有实时进程在linux上面进行运行 这边来看看实时进程在linux里面怎么调度 同时linux操作系统对实时任务的处理方式和设计思想 实时调度类 Linux进程分为两大类:实时进程和普通进程。 实时进程与普通进程根本不同之处&#xff0c;如果系…

ModuleNotFoundError: No module named ‘transformers_modules.chatglm2-6b‘解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

青少年机器人技术一级核心知识点:机械结构及模型(四)

随着科技的不断进步&#xff0c;机器人技术已经成为了一个重要的领域。在这个领域中&#xff0c;机械结构是机器人设计中至关重要的一部分&#xff0c;它决定了机器人的形态、运动方式和工作效率。对于青少年机器人爱好者来说&#xff0c;了解机械结构的基础知识&#xff0c;掌…

LabVIEW大模拟数据解决方案

LabVIEW大模拟数据解决方案 比亚迪汽车对于在动力总成标定和控制部门工作的400多名工程师来说&#xff0c;这种投资包括实现NI的新战略和解决方案&#xff0c;以更好地捕获和管理大量的原始测试数据&#xff0c;在车辆上市前做出更明智的决策。 因为能够更好地获取更优质的数…

Go语言开发者的Apache Arrow使用指南:内存管理

如果你看了上一篇《Go语言开发者的Apache Arrow使用指南&#xff1a;数据类型》[1]中的诸多Go操作arrow的代码示例&#xff0c;你很可能会被代码中大量使用的Retain和Release方法搞晕。不光大家有这样的感觉&#xff0c;我也有同样的feeling&#xff1a;**Go是GC语言[2]&#x…

MWCS 2023,到底有些啥?(下篇)

█ 亚信科技 5G行业专网一体机&#xff1a; 反光太厉害了&#xff0c;看不太清&#xff1a; 这几张都是小枣妹拍的&#xff0c;^_^&#xff1a; █ 浩鲸科技 浩鲸&#xff0c;就是以前的中兴软创&#xff1a; █ 紫光展锐 6G这块&#xff0c;干货很多&#xff1a; 这次重点展示…