概述
上篇文章,我们学习了访问者模式的原理和实现,并还原了访问者模式诞生的过程。总体来说,这个模式的代码实现比较难,所以应用场景不多。从应用开发的角度来说,它的确不是我们学习的重点。
本章,我们把访问者模式作为引子,一块讨论下这样两个问题:
- 为什么支持双分派的语言不需要访问者模式?
- 除了访问者模式,上篇文章的例子还有其他的实现方式吗?
为什么支持双分派的语言不需要访问者模式?
实际上,讲到访问者模式,大部分书籍或资料都会讲到 Double Dispatch,中文翻译为双分派。虽然学习访问者模式,并不用非得理解这个概念,但是为了让你在查看其他书籍或资料时,不会卡在这个概念上,本章在这里讲一下。
此外,个人觉得,学习 Double DIspatch 可以加深你对访问者模式的理解。
既然有 Double Dispatch,对应的就有 Signle Dispatch。
- 所谓 Signle Dispatch ,指的是执行哪个对象的方法 ,根据对象的运行时类型来决定;执行对象的哪个方法 ,根据方法参数的
编译时
类型来决定。 - 所谓 Double Dispatch ,指的是执行哪个对象的方法 ,根据对象的运行时类型来决定;执行对象的哪个方法 ,根据方法参数的
运行时
类型来决定。
如何理解 "Dispatch" 这个单词呢? 在面向对象编程语言中,可以把方法调用理解为一种消息传递,也就是 "Dispatch"。一个对象调用另一个对象的方法,就相当于给它发送一条消息。这条消息要包含对象名、方法名、方法参数。
如何理解 "Single" "Double" 这两个单词呢? "Single" "Double" 指的是执行那个对象的哪个方法,跟几个因素的运行时类型有关。
- Signle Dispatch 之所以称为 "Single",是因为执行哪个对象的哪个方法,只跟 "对象" 的运行时类型有关。
- Double Dispatch 之所以称为 "Double",是因为执行哪个对象的哪个方法,跟 "对象" 和 "方法参数" 两者的运行时类型有关。
具体到编程语言的语法机制,Signle Dispatch 和 Double Dispatch 跟多态和函数重载直接相关。当前主流的面向对象编程语言(比如,Java、C++、C#)都只支持 Signle Dispatch ,不支持 Double Dispatch。
接下来,拿 Java 语言来举例说明下。
Java 支持多态,代码可以在运行时获得对象的实际类型(也就是前面提到的运行时类型),然后根据实际类型决定调用哪个方法。尽管 Java 支持函数重载,但 Java 设计的函数重载,并不是在运行时,根据传递进函数的参数的实际类型,来决定调用重载函数。而是在编译时,根据传递进函数的参数的声明类型(也就是前面提到的编译时类型),来决定调用哪个重载函数。也就是说,具体执行哪个对象的哪个方法,只跟对象的运行时类型有关,跟函数参数的运行时类型无关。所以,Java 语言只支持 Signle Dispatch 。
这么说可能比较抽象,下面举个例子来说明下。
java
public class ParentClass {
public void f() {
System.out.println("I am ParentClass's f().");
}
}
public class ChildClass extends ParentClass {
@Override
public void f() {
System.out.println("I am ChildClass's f().");
}
}
public class SingleDispatchCLass {
public void polymorphismFunction(ParentClass p) {
p.f();
}
public void overloadsFunction(ParentClass p) {
System.out.println("I am overloadFunction(ParentClass p)");
}
public void overloadsFunction(ChildClass c) {
System.out.println("I am overloadFunction(ChildClass c)");
}
}
public class DemoMain {
public static void main(String[] args) {
SingleDispatchCLass demo = new SingleDispatchCLass();
ParentClass p = new ChildClass();
demo.polymorphismFunction(p); // 执行哪个对象的方法,由对象的实际类型决定
demo.overloadsFunction(p); // 执行对象的哪个方法,由参数对象的声明类型决定
}
}
// 代码执行结果:
I am ChildClass's f().
I am overloadFunction(ParentClass p)
上面的代码中,demo.polymorphismFunction(p)
执行 p 的实际类型的 f()
函数,也就是 ChildClass
的 f()
函数。 demo.overloadsFunction(p)
匹配的是重载函数中的 overloadsFunction(ParentClass p)
,也就是根据 p 的声明类型来决定匹配哪个重载函数。
假设 Java 语言支持 Double Dispatch,那下面的代码(摘抄至上篇文章)中 extractor.extract2txt(resourceFile)
的就不会报错。代码运行时,根据参数(resourceFile
)的实际类型(PdfFile
、PptFile
、WordFile
),来决定使用 extract2txt
的三个重载函数中的哪一个。下面的代码就能正常运行了,也就不需要访问者模式了。这也回达了为什么支持 Double Dispatch 的语言不需要访问者模式。
java
public abstract class ResourceFile {
protected String filePath;
public ResourceFile(String filePath) {
this.filePath = filePath;
}
}
public class PptFile extends ResourceFile {
public PptFile(String filePath) {
super(filePath);
}
// ...
}
public class PdfFile extends ResourceFile {
public PptFile(String filePath) {
super(filePath);
}
// ...
}
public class WordFile extends ResourceFile {
public PptFile(String filePath) {
super(filePath);
}
// ...
}
public class Extractor {
public void extract2txt(PptFile pptFile) {
// ...
System.out.println("Extract PPT.");
}
public void extract2txt(PdfFile pdfFile) {
// ...
System.out.println("Extract PDF.");
}
public void extract2txt(WordFile wordFile) {
// ...
System.out.println("Extract Word.");
}
}
public class ToolApplication {
public static void main(String[] args) {
Extractor extractor = new Extractor();
List<ResourceFile> resourceFiles = listAllResourceFiles(args[0]);
for (ResourceFile resourceFile : resourceFiles) {
extractor.extract2txt(resourceFile);
}
}
private static List<ResourceFile> listAllResourceFiles(String resourceDirectory) {
List<ResourceFile> resourceFiles = new ArrayList<>();
// ... 根据后缀(ppt/pdf/word)由工厂方法创建不同类型的类对象(PptFile/PdfFile/WordFile)
resourceFiles.add(new PdfFile("a.pdf"));
resourceFiles.add(new PptFile("b.ppt"));
resourceFiles.add(new WordFile("c.word"));
return resourceFiles;
}
}
除了访问者模式,上一节的例子还有其他的实现方案吗?
上篇文章,通过一个例子给你展示了,访问者模式是如何一步一步设计出来的。我们在回顾下那个例子。我们从网址站上爬取了很多资源文件,它们的格式有:PDF、PPT、Word。我们要开发一个工具来处理这批资源文件,这其实就包含抽取文本内容、压缩资源文件、提取文件信息等。
实际上,开发这个工具有很多种代码设计和实现思路。为了讲解访问者模式,上篇文章,我们使用了访问者模式来实现。实际上,还有其他的实现方法,比如,可以利用工程模式来实现,定义一个包含 extract2txt()
函数的 Extractor
接口。PdfExtractor
、PptExtractor
、WordExtractor
实现 Extractor
接口,并且在各自的 extract2txt()
函数中,分别实现 pdf、ppt、word 格式文件的文本内容抽取 。ExtractorFactory
工厂类根据不同的文件类型,返回不同的 Extractor
。
这个实现思路其实更加简单,代码如下所示。
java
public enum ResourceFileType {
PDF,
PPT,
WORD;
}
public abstract class ResourceFile {
protected String filePath;
public ResourceFile(String filePath) {
this.filePath = filePath;
}
public abstract ResourceFileType getType();
}
public class PdfFile extends ResourceFile {
public PdfFile(String filePath) {
super(filePath);
}
@Override
public ResourceFileType getType() {
return ResourceFileType.PDF;
}
// ...
}
public class PptFile extends ResourceFile {
public PptFile(String filePath) {
super(filePath);
}
@Override
public ResourceFileType getType() {
return ResourceFileType.PPT;
}
// ...
}
public class WordFile extends ResourceFile {
public WordFile(String filePath) {
super(filePath);
}
@Override
public ResourceFileType getType() {
return ResourceFileType.WORD;
}
// ...
}
public interface Extractor {
void extract2txt(ResourceFile resourceFile);
}
public class PdfExtractor implements Extractor {
@Override
public void extract2txt(ResourceFile resourceFile) {
// ...
}
}
public class PptExtractor implements Extractor {
@Override
public void extract2txt(ResourceFile resourceFile) {
// ...
}
}
public class WordExtractor implements Extractor {
@Override
public void extract2txt(ResourceFile resourceFile) {
// ...
}
}
public class ExtractorFactory {
private static final Map<ResourceFileType, Extractor> extractors = new HashMap<>();
static {
extractors.put(ResourceFileType.PDF, new PdfExtractor());
extractors.put(ResourceFileType.PPT, new PptExtractor());
extractors.put(ResourceFileType.WORD, new WordExtractor());
}
public static Extractor getExtractor(ResourceFileType type) {
return extractors.get(type);
}
}
public class ToolApplication {
public static void main(String[] args) {
List<ResourceFile> resourceFiles = listAllResourceFiles(args[0]);
for (ResourceFile resourceFile : resourceFiles) {
Extractor extractor = ExtractorFactory.getExtractor(resourceFile.getType());
extractor.extract2txt(resourceFile);
}
}
private static List<ResourceFile> listAllResourceFiles(String resourceDirectory) {
List<ResourceFile> resourceFiles = new ArrayList<>();
// ... 根据后缀(ppt/pdf/word)由工厂方法创建不同类型的类对象(PptFile/PdfFile/WordFile)
resourceFiles.add(new PdfFile("a.pdf"));
resourceFiles.add(new PptFile("b.ppt"));
resourceFiles.add(new WordFile("c.word"));
return resourceFiles;
}
}
当需要添加新功能时,比如压缩文件,类似抽取文本内容功能的实现代码,只需要添加一个 Compressor
接口,PdfCompressor
、PptCompressor
、WordCompressor
三个实现类,以及创建它们的 CompressorFactory
工厂类即可。唯一需要修改的只有最上层的 ToolApplication
。基本上符合 "对扩展开放、对修改关闭" 的设计原则。
- 对于资源文件处理工具的例子,如果工具提供的功能并不是很多,只有几个而已,那更推荐使用工程模式的实现方式,比较代码清晰、易懂。
- 相反,如果工具提供非常多的功能,比如有十几个,那更推荐使用访问者模式,因为访问者模式需要定义的类要比工程模式的少很多,类太多也会影响代码的可维护性。
总结
总体来说,访问者模式难以理解,应用场景有限,不是特别必需,不建议在项目中使用它。所以,对于上篇文章的处理资源文件的例子,更推荐使用工厂设计模式来设计和实现。
本章重点讲解了 Double Dispatch。在面向对象编程语言中,方法调用可以理解为一种消息传递(Dispatch)。一个对象调用另一个对象的方法,就相当于给它发送一条消息,这条消息起码要包含对象名、方法名和方法参数。
- 所谓 Signle Dispatch,指的是执行哪个对象的方法,根据对象的运行时类型来决定;执行对象的哪个方法,根据方法参数的编译时类型来决定。
- 所谓 Double Dispatch,指的是执行哪个对象的方法,根据对象的运行时类型来决定;执行对象的哪个方法,根据方法参数的运行时类型来决定。
具体到编程语言的语法机制,Signle Dispatch 和 Double Dispatch 跟多态和函数重载直接相关。当前主流的面向对象编程语言(如,Java、C++)都只支持 Signle Dispatch,不支持 Double Dispatch。