mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-25 04:17:17 +00:00
321 lines
13 KiB
Markdown
321 lines
13 KiB
Markdown
# 访问者模式
|
||
|
||
> 访问者模式可以算是 23 种经典设计模式中最难理解的几个之一。因为它难理解、难实现,应用它会导致代码的可读性、可维护性变差,所以,访问者模式在实际的软件开发中很少被用到,在没有特别必要的情况下,建议你不要使用访问者模式
|
||
|
||
## 定义
|
||
访问者者模式的英文翻译是 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.
|
||
翻译成中文就是:允许一个或者多个操作应用到一组对象上,解耦操作和对象本身。
|
||
|
||
## 实现
|
||
### 场景一
|
||
假设我们从网站上爬取了很多资源文件,它们的格式有三种:PDF、PPT、Word。我们现在要开发一个工具来处理这批资源文件。这个工具的其中一个功能是,把这些资源文件中的文本内容抽取出来放到 txt 文件中。
|
||
|
||
```
|
||
public abstract class ResourceFile {
|
||
protected String filePath;
|
||
public ResourceFile(String filePath) {
|
||
this.filePath = filePath;
|
||
}
|
||
public abstract void extract2txt();
|
||
}
|
||
|
||
public class PPTFile extends ResourceFile {
|
||
public PPTFile(String filePath) {
|
||
super(filePath);
|
||
}
|
||
@Override
|
||
public void extract2txt() {
|
||
//...省略一大坨从PPT中抽取文本的代码...
|
||
//...将抽取出来的文本保存在跟filePath同名的.txt文件中...
|
||
System.out.println("Extract PPT.");
|
||
}
|
||
}
|
||
public class PdfFile extends ResourceFile {
|
||
public PdfFile(String filePath) {
|
||
super(filePath);
|
||
}
|
||
@Override
|
||
public void extract2txt() {
|
||
//...
|
||
System.out.println("Extract PDF.");
|
||
}
|
||
}
|
||
public class WordFile extends ResourceFile {
|
||
public WordFile(String filePath) {
|
||
super(filePath);
|
||
}
|
||
@Override
|
||
public void extract2txt() {
|
||
//...
|
||
System.out.println("Extract WORD.");
|
||
}
|
||
}
|
||
|
||
public class ToolApplication {
|
||
public static void main(String[] args) {
|
||
List<ResourceFile> resourceFiles = listAllResourceFiles(args[0]);
|
||
for (ResourceFile resourceFile : resourceFiles) {
|
||
resourceFile.extract2txt();
|
||
}
|
||
}
|
||
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;
|
||
}
|
||
}
|
||
```
|
||
问题分析:
|
||
如果工具的功能不停地扩展,不仅要能抽取文本内容,还要支持压缩、提取文件元信息(文件名、大小、更新时间等等)构建索引等一系列的功能,那如果我们继续按照上面的实现思路,就会存在这样几个问题
|
||
- 违背开闭原则,添加一个新的功能,所有类的代码都要修改
|
||
- 功能增多,每个类的代码都不断膨胀,可读性和可维护性都变差了
|
||
- 把所有比较上层的业务逻辑都耦合到 PdfFile、PPTFile、WordFile 类中,导致这些类的职责不够单一,变成了大杂烩
|
||
|
||
解法:解决方法就是拆分解耦,把业务操作跟具体的数据结构解耦,设计成独立的类。
|
||
```
|
||
public abstract class ResourceFile {
|
||
protected String filePath;
|
||
public ResourceFile(String filePath) {
|
||
this.filePath = filePath;
|
||
}
|
||
}
|
||
public class PdfFile extends ResourceFile {
|
||
public PdfFile(String filePath) {
|
||
super(filePath);
|
||
}
|
||
//...
|
||
}
|
||
//...PPTFile、WordFile代码省略...
|
||
|
||
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 resourceDirecto
|
||
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;
|
||
}
|
||
}
|
||
```
|
||
其中核心的就是将抽取文本的操作,设计成了3个重载函数(在同一个类中,函数名相同、参数不同的一组函数)
|
||
不过上面的代码还是有问题 `extractor.extract2txt(resourceFile)` 会报错。resourceFiles 包含的对象的声明类型都是 ResourceFile,而我们并没有在 Extractor 类中定义参数类型是 ResourceFile 的 extract2txt() 重载函数,所以在编译阶段就通过不了,更别说在运行时根据对象的实际类型执行不同的重载函数了
|
||
|
||
改进
|
||
```
|
||
public abstract class ResourceFile {
|
||
protected String filePath;
|
||
public ResourceFile(String filePath) {
|
||
this.filePath = filePath;
|
||
}
|
||
abstract public void accept(Extractor extractor);
|
||
}
|
||
public class PdfFile extends ResourceFile {
|
||
public PdfFile(String filePath) {
|
||
super(filePath);
|
||
}
|
||
@Override
|
||
public void accept(Extractor extractor) {
|
||
extractor.extract2txt(this);
|
||
}
|
||
//...
|
||
}
|
||
//...PPTFile、WordFile跟PdfFile类似,这里就省略了...
|
||
//...Extractor代码不变...
|
||
|
||
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);
|
||
}
|
||
}
|
||
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;
|
||
}
|
||
}
|
||
```
|
||
`resourceFile.accept(extractor);` 根据多态特性,会调用实际的 accept 函数,比如 PdfFile 的 accept 函数,其中的 this 也就是 PdfFile 类型的,然后调用 accept 传递进来的 extractor 的 extract2txt 方法的 this 也就是 PdfFile,调用到 extractor 的 `public void extract2txt(PPTFile pptFile)` 方法。这也是访问者模式的精髓(也是实际场景中难以想到要问访问者模式的原因,可能是随机想到的刚好命中访问者模式)
|
||
|
||
|
||
### 场景二
|
||
如果要继续添加新的功能,比如前面提到的压缩功能,根据不同的文件类型,使用不同的压缩算法来压缩资源文件,那我们该如何实现呢?
|
||
1. 需要实现一个类似 Extractor 类的新类 Compressor 类,在其中定义三个重载函数,实现对不同类型资源文件的压缩
|
||
2. 还要在每个资源文件类中定义新的 accept 重载函数
|
||
|
||
```
|
||
public abstract class ResourceFile {
|
||
protected String filePath;
|
||
public ResourceFile(String filePath) {
|
||
this.filePath = filePath;
|
||
}
|
||
abstract public void accept(Extractor extractor);
|
||
abstract public void accept(Compressor compressor);
|
||
}
|
||
public class PdfFile extends ResourceFile {
|
||
public PdfFile(String filePath) {
|
||
super(filePath);
|
||
}
|
||
@Override
|
||
public void accept(Extractor extractor) {
|
||
extractor.extract2txt(this);
|
||
}
|
||
@Override
|
||
public void accept(Compressor compressor) {
|
||
compressor.compress(this);
|
||
}
|
||
//...
|
||
}
|
||
|
||
//...PPTFile、WordFile跟PdfFile类似,这里就省略了...
|
||
//...Extractor代码不变
|
||
|
||
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<>();
|
||
//...根据后缀(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;
|
||
}
|
||
}
|
||
```
|
||
问题分析:代码还存在一些问题,添加一个新的业务,还是需要修改每个资源文件类,违反了开闭原则。
|
||
|
||
针对这个问题,我们抽象出来一个 Visitor 接口,包含是三个命名非常通用的 visit() 重载函数,分别处理三种不同类型的资源文件。具体做什么业务处理,由实现这个 Visitor 接口的具体的类来决定,比如 Extractor 负责抽取文本内容,Compressor 负责压缩。当我们新添加一个业务功能的时候,资源文件类不需要做任何修改,只需要修改 ToolApplication 的代码就可以了。
|
||
```
|
||
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) {
|
||
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<>();
|
||
//...根据后缀(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;
|
||
}
|
||
}
|
||
```
|
||
|
||
## 使用场景
|
||
访问者模式针对的是一组类型不同的对象(PdfFile、PPTFile、WordFile)。不过,尽管这组对象的类型是不同的,但是,它们继承相同的父类(ResourceFile)或者实现相同的接口。在不同的应用场景下,我们需要对这组对象进行一系列不相关的业务操作(抽取文本、压缩等),但为了避免不断添加功能导致类(PdfFile、PPTFile、WordFile)不断膨胀,职责越来越不单一,以及避免频繁地添加功能导致的频繁代码修改,我们使用访问者模式,将对象与操作解耦,将这些业务操作抽离出来,定义在独立细分的访问者类(Extractor、Compressor)中
|
||
|
||
|
||
访问者模式允许一个或者多个操作应用到一组对象上,设计意图是解耦操作和对象本身,保持类职责单一、满足开闭原则以及应对代码的复杂性。
|
||
|