访问者模式。它可以算是 23 种经典设计模式中最难理解的几个之一。因为它难理解、难实现,应用它会导致代码的可读性、可维护性变差,所以,访问者模式在实际的软件开发中很少被用到,在没有特别必要的情况下,建议你不要使用访问者模式。
一个案例
假设我们从网站上爬取了很多资源文件,它们的格式有三种:PDF、PPT、Word。我们现在要开发一个工具来处理这批资源文件。这个工具的其中一个功能是,把这些资源文件中的文本内容抽取出来放到 txt 文件中。如果让你来实现,你会怎么来做呢?
我们抽象出来一个 Visitor 接口,包含是三个命名非常通用的 visit() 重载函数,分别处理三种不同类型的资源文件。具体做什么业务处理,由实现这个 Visitor 接口的具体的类来决定,比如 Extractor 负责抽取文本内容,Compressor 负责压缩。当我们新添加一个业务功能的时候,资源文件类不需要做任何修改,只需要修改 ToolApplication 的代码就可以了。
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
| 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); }
}
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) { Extractor extractor = new Extractor(); List<ResourceFile> resourceFiles = listAllResourceFiles(args[0]); 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<>(); resourceFiles.add(new PdfFile("a.pdf")); resourceFiles.add(new WordFile("b.word")); resourceFiles.add(new PPTFile("c.ppt")); return resourceFiles; } }
|
访问者者模式的英文翻译是 Visitor Design Pattern。在 GoF 的《设计模式》一书中,它是这么定义的:
Allows for one or more operation to be applied to a set of objects at runtime, decoupling the operations from the object structure.
翻译成中文就是:允许一个或者多个操作应用到一组对象上,解耦操作和对象本身。
一般来说,访问者模式针对的是一组类型不同的对象(PdfFile、PPTFile、WordFile)。不过,尽管这组对象的类型是不同的,但是,它们继承相同的父类(ResourceFile)或者实现相同的接口。在不同的应用场景下,我们需要对这组对象进行一系列不相关的业务操作(抽取文本、压缩等),但为了避免不断添加功能导致类(PdfFile、PPTFile、WordFile)不断膨胀,职责越来越不单一,以及避免频繁地添加功能导致的频繁代码修改,我们使用访问者模式,将对象与操作解耦,将这些业务操作抽离出来,定义在独立细分的访问者类(Extractor、Compressor)中。
为什么支持双分派的语言不需要访问者模式?
在面向对象编程语言中,方法调用可以理解为一种消息传递(Dispatch)。一个对象调用另一个对象的方法,就相当于给它发送一条消息,这条消息起码要包含对象名、方法名和方法参数。
所谓 Single Dispatch,指的是执行哪个对象的方法,根据对象的运行时类型来决定;执行对象的哪个方法,根据方法参数的编译时类型来决定。所谓 Double Dispatch,指的是执行哪个对象的方法,根据对象的运行时类型来决定;执行对象的哪个方法,根据方法参数的运行时类型来决定。
具体到编程语言的语法机制,Single Dispatch 和 Double Dispatch 跟多态和函数重载直接相关。当前主流的面向对象编程语言(比如,Java、C++、C#)都只支持 Single Dispatch,不支持 Double Dispatch。
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
| public class ParentClass { public void f() { System.out.println("I am ParentClass's f()."); } }
public class ChildClass extends ParentClass { public void f() { System.out.println("I am ChildClass's f()."); } }
public class SingleDispatchClass { public void polymorphismFunction(ParentClass p) { p.f(); }
public void overloadFunction(ParentClass p) { System.out.println("I am overloadFunction(ParentClass p)."); }
public void overloadFunction(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.overloadFunction(p); } }
I am ChildClass's f(). I am overloadFunction(ParentClass p).
|
除了访问者模式,上一节的例子还有其他实现方案吗?
我们还可以利用工厂模式来实现,定义一个包含 extract2txt() 接口函数的 Extractor 接口。PdfExtractor、PPTExtractor、WordExtractor 类实现 Extractor 接口,并且在各自的 extract2txt() 函数中,分别实现 Pdf、PPT、Word 格式文件的文本内容抽取。ExtractorFactory 工厂类根据不同的文件类型,返回不同的 Extractor。
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
| 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 interface Extractor { void extract2txt(ResourceFile resourceFile); }
public class PdfExtractor 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<>(); resourceFiles.add(new PdfFile("a.pdf")); resourceFiles.add(new WordFile("b.word")); resourceFiles.add(new PPTFile("c.ppt")); return resourceFiles; } }
|
当需要添加新的功能的时候,比如压缩资源文件,类似抽取文本内容功能的代码实现,我们只需要添加一个 Compressor 接口,PdfCompressor、PPTCompressor、WordCompressor 三个实现类,以及创建它们的 CompressorFactory 工厂类即可。唯一需要修改的只有最上层的 ToolApplication 类。基本上符合“对扩展开放、对修改关闭”的设计原则。
对于资源文件处理工具这个例子,如果工具提供的功能并不是非常多,只有几个而已,那我更推荐使用工厂模式的实现方式,毕竟代码更加清晰、易懂。相反,如果工具提供非常多的功能,比如有十几个,那我更推荐使用访问者模式,因为访问者模式需要定义的类要比工厂模式的实现方式少很多,类太多也会影响到代码的可维护性。
参考
设计模式之美_设计模式_代码重构-极客时间
https://time.geekbang.org/column/intro/250