访问者模式

在实际工作中经常用到访问者模式,是比较常见的设计模式,本文主要通过以下几个方面来学习访问者模式:

  1. 什么是访问者模式,访问者模式想要解决的问题是什么?
  2. 访问者模式的经典应用有哪些?

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对象的运行时的实际类型来做类型的判断,这里就会有很多instanceofelse 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 总结

利用访问者模式来解决这样的双重分派问题,如上面的类图所示,通过几个角色来做功能的区分,将文件的访问和处理分离成两个独立的接口。

image.png

  • 访问者(Visitor),这里指不同类型的文件操作。用一个接口和一组不同类型的具体实现来定义不同的操作类型。
  • 被访问者(Element),这里指不同类型的文件。定义了accept操作,以Visitor作为参数,来接受不同类型visitor的对象访问。在accept方法中将this传递给访问者,通过回调再回调的操作,实现了双重分派。
  • 对象结构(ObjectStructure),访问的组织者,可以是组合也可以是集合;能够枚举它包含的元素;提供一个接口,允许Vistor访问它的元素。

理解访问者模式的设计,我觉得重点在理解所谓的回调再回调。这里有两次回调就意味着有两种类型接口,第一次回调是被访问者通过accept接受访问者,第二次回调是访问者通过visit方法访问被访问者,通过两次互相调换类型的调用,也就是通过两次单分派实现了双重分派。

参考文档

王争<<设计模式之美>>
https://www.jianshu.com/p/cd17bae4e949
https://www.liaoxuefeng.com/wiki/1252599548343744/1281319659110433