这是一个难以理解的设计模式,难理解,难实现,应用其会导致代码的可读性降低,可维护性降低,所以一般不用访问者模式

在经典的设计模式一书中,其定义为 Visitor Design Pattern ,具体的含义为 允许一个或者多个操作应用到同一组对象上,解耦操作和对象本身

对于这个设计模式,个人觉着更适合使用一个实际的案例去理解

假设,我们在网上爬了很多的资源文件,格式有三种 PDF PPT WORD,我们要开发一个工具去处理这批文件,其中一个功能就是抽取不同类型的文件为txt文件,让我们实现,如何做?

实现这个功能不难.最简单的写法,我们抽象出一个ResourceFile类,包含一个抽象函数 extract2txt()函数,PdfFile,PPTFile,WordFile都继承自ResourceFile类,都重写了extract2txt()函数,根据多台的特性,根据对象的实际类型来执行不同的方法

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.”);

}

}

// 运行结果是:

// Extract PDF.

// Extract WORD.

// Extract PPT.

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等类中,职责不够单一

于是,我们应该拆分解耦,将业务操作跟具体的数据结构解耦,设计成为独立的类,那么继续重构

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 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;

}

}

其中关键的一点就是,我们将抽取的文本操作,设计为了三个重载的函数,但是上面的代码是走不通的,因为,多台是一种动态的绑定,可以在运行的时候获取到时机类型,编译的时候并不能获取到实际类型,而是根据声明类型执行声明类型对应的方法

我们只是声明了ResouceFile,但是在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;

}

}

我们会调用实际类的accept函数,比如PdfFile的accept函数,这个accpet是类内部的,也就是编译时候就是编译了,实现的思路有一些技巧,这是访问者模式的关键所在

现在,如果需要添加新的功能,比如前面的压缩的功能,使用不同的压缩算法来压缩源文件,如何实现呢?

我们需要定义一个新的类Compressor类,在定义三个重载函数,实现对不同资源文件的压缩除此外,我们还需要在每个资源类中定义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()重载函数,处理不同的资源文件,具体处理什么业务,交给实现类,这样在新增加一个功能的时候,只要修改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;

}

}

那么说完了,整个访问者模式,其实就很显然了

我们最终的类图就是如下了

图片

其使用场景很简单了,就是对一组不同的对象,对这组对象进行一系列业务无关的操作,为了避免不断添加功能导致类的膨胀,使用了访问者模式,进行抽离,定义在独立的访问者类中

本章重点

这是一个不易理解的设计模式,一般没有必要去使用其

其主要是为了一个或者多个操作应用到一组对象上,设计意图是为了解耦操作或者对象本身,主要是为了保证类的单一,满足开闭原则,对于访问者模式,主要就是对于函数的重载问题的思考,什么时候是静态的,什么是动态的

课后思考

1.今天的例子能不能不用访问者模式来搞定

1.能,只需要应用策略模式,在策略类中提供一个入口,在入口方法中进行类的判断,调用即可,可读性较好,不过代码比较冗余

2.对于本章,其实可看做就是不同类型的用户去访问一个服务器,根据不同的用户去做不同的处理.比较困难的就是用户可能不知道自己是谁,那就需要我们去帮他知道是谁

发表评论

邮箱地址不会被公开。 必填项已用*标注