在实际工作中经常用到访问者模式,是比较常见的设计模式,本文主要通过以下几个方面来学习访问者模式:
- 什么是访问者模式,访问者模式想要解决的问题是什么?
- 访问者模式的经典应用有哪些?
0x01 单分派和双重分派
在介绍设计模式之前,先了解几个基础的概念。了解概念的含义并不是为了咬文嚼字,而是希望能从原理上理解设计模式背后想要解决的问题
重写(override)和重载(overload)
- 重写,就是子类重写了父类的方法,返回值和形参都不能改变。当子类对象调用重写的方法时,调用的是子类的方法,而不是父类中被重写的方法。
- 重载,在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。
单分派和双重分派
- 分派(Dispatch),在面向对象的语言中,可以把一次函数调动理解成一个消息事件的分发,如
a.test(b)
,a就是消息的接受者,这个函数的调用方就是消息的发送者。
- 单分派(Single Dispatch),这里的单(Single)指的是,哪个对象的方法会被执行,只跟这个对象的运行时类型有关。以
a.test(b)
为例,如在Java中,在被执行的test函数,只跟a对象的运行时类型有关。
- 双重分派(Double Dispatch),这里的双(double)指的是,哪个对象的方法被执行,跟对象和方法参数的运行时类型都有关。还是以
a.test(b)
为例,哪个test函数被执行,不单单和a对象的类型有关还和b对象的类型有关。
可以看到所谓分派就是函数的调用,所谓单分派和双分派就是和语言的多态特性有关,在常见的Java,C++,C#语言中,在语言层面都是只支持单分派的。想要实现双重分派,就要借助设计模式,比如访问者模式。你肯定会问,双重分派的作用是什么?不解决双重分派的问题不行吗?其实这种问题在项目代码中一定俯拾皆是,类似下面的这种代码,我们想要针对不同类型的文件(pdf,ppt,word)执行不同的文件提取和文件压缩操作。试想下,可能是你来实现,你要怎么做?是不是很容易写出下面这样的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class Extractor extends Processor { void processFile(ResourceFile file) { if (file instanceof PdfFile) { processPrdFile((PdfFile)file); } else if (file instanceof PowerPointFile) { processPowerPointFile((PowerPointFile)e); } else if (file instanceof WordFile) { processWordFile((WordFile)e); } } }
class Compressor extends Processor { void processFile(ResourceFile file) { if (file instanceof PdfFile) { processPrdFile((PdfFile)file); } else if (file instanceof PowerPointFile) { processPowerPointFile((PowerPointFile)e); } else if (file instanceof WordFile) { processWordFile((WordFile)e); } } }
|
可以看到这段代码的逻辑执行,既要根据接收者的运行时类型来决定processXXXFile(file)
的执行,这里的接收者可以理解成是当前方法所对应的Processor
对象。又要根据ResourceFile file
对象的运行时的实际类型来做类型的判断,这里就会有很多instanceof
和else if
,switch case
的多重嵌套。这种设计的代码虽然可以实现功能,但是在面对需求变更和扩展时会非常不灵活,既要加很多else if
,也不利于功能的内聚和复用。那么这些代码如果用访问者模式,应该怎么来实现呢?这里借用王争在<<设计模式之美>>中的实例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
| public abstract class ResourceFile { protected String filePath; public ResourceFile(String filePath) { this.filePath = filePath; } abstract public void accept(Visitor vistor); }
public class PdfFile extends ResourceFile { public PdfFile(String filePath) { super(filePath); }
@Override public void accept(Visitor visitor) { visitor.visit(this); }
//... } //...PPTFile、WordFile跟PdfFile类似,这里就省略了...
public interface Visitor { void visit(PdfFile pdfFile); void visit(PPTFile pdfFile); void visit(WordFile pdfFile); }
public class Extractor implements Visitor { @Override public void visit(PPTFile pptFile) { //... System.out.println("Extract PPT."); }
@Override public void visit(PdfFile pdfFile) { //... System.out.println("Extract PDF."); }
@Override public void visit(WordFile wordFile) { //... System.out.println("Extract WORD."); } }
public class Compressor implements Visitor { @Override public void visit(PPTFile pptFile) { //... System.out.println("Compress PPT."); }
@Override public void visit(PdfFile pdfFile) { //... System.out.println("Compress PDF."); }
@Override public void visit(WordFile wordFile) { //... System.out.println("Compress WORD."); }
}
public class ToolApplication { public static void main(String[] args) { List<ResourceFile> resourceFiles = listAllResourceFiles(args[0]);
Extractor extractor = new Extractor(); for (ResourceFile resourceFile : resourceFiles) { resourceFile.accept(extractor); }
Compressor compressor = new Compressor(); for(ResourceFile resourceFile : resourceFiles) { resourceFile.accept(compressor); } }
private static List<ResourceFile> listAllResourceFiles(String resourceDirectory) { List<ResourceFile> resourceFiles = new ArrayList<>(); //...根据后缀(pdf/ppt/word)由工厂方法创建不同的类对象(PdfFile/PPTFile/WordFile) resourceFiles.add(new PdfFile("a.pdf")); resourceFiles.add(new WordFile("b.word")); resourceFiles.add(new PPTFile("c.ppt")); return resourceFiles; } }
|
0x02 ASM中的访问者模式
上面介绍了访问者模式设计初衷和设计方法,这里再看下访问者模式在实际工程中的应用。访问者模式最常见的应用场景就是访问复杂的结构或者对象,在不改变数据结构的情况下,将数据访问和数据操作分离出来,用回调的方式在访问者中处理业务逻辑。在面对不同的访问处理时,只需要新定义一个访问者实现不同的访问处理逻辑就可以了。这样说可能也很抽象,可以在在ASM中,是如何利用访问者的设计模式,实现字节码文件的读取和修改的。
ASM使用ClassReader遍历class文件结构获取文件中的类和对象信息,在其accept方法中接收ClassVisitor,在ClassVisitor的不同回调方法中完成不同的字节码操作。可以通过代码示例看到主要有以下几个类:
1 2 3 4 5 6
| ClassReader cr = new ClassReader(inputStream); ClassWriter cw = new ClassWriter(cr, 0);
ClassVisitor cv = new InjectCassVisitor(ASM6, cw, methodName); cr.accept(cv, 0); return cw.toByteArray();
|
- ClassReader(Element):将不同输入类型的字节码读取到内存中,通过accept方法接受ClassVisitor的访问。
- ClassVisitor(Visitor):完全由开发者自定义不同类型的Visitor,在Visitor的visitXXX回调中接收读取到的字节码信息并进行相应的处理。
0x03 总结
利用访问者模式来解决这样的双重分派问题,如上面的类图所示,通过几个角色来做功能的区分,将文件的访问和处理分离成两个独立的接口。
- 访问者(Visitor),这里指不同类型的文件操作。用一个接口和一组不同类型的具体实现来定义不同的操作类型。
- 被访问者(Element),这里指不同类型的文件。定义了accept操作,以Visitor作为参数,来接受不同类型visitor的对象访问。在accept方法中将this传递给访问者,通过回调再回调的操作,实现了双重分派。
- 对象结构(ObjectStructure),访问的组织者,可以是组合也可以是集合;能够枚举它包含的元素;提供一个接口,允许Vistor访问它的元素。
理解访问者模式的设计,我觉得重点在理解所谓的回调再回调。这里有两次回调就意味着有两种类型接口,第一次回调是被访问者通过accept接受访问者,第二次回调是访问者通过visit方法访问被访问者,通过两次互相调换类型的调用,也就是通过两次单分派实现了双重分派。
参考文档
王争<<设计模式之美>>
https://www.jianshu.com/p/cd17bae4e949
https://www.liaoxuefeng.com/wiki/1252599548343744/1281319659110433