设计模式:访问者模式

访问者模式在实际的软件开发中很少被用到,在没有特别必要的情况下,建议你不要使用访问者模式。

带你“发明”访问者模式

假设我们从网站上爬取了很多资源文件,它们的格式有三种:PDF、PPT、Word。我们现在要开发一个工具来处理这批资源文件,这个工具的其中一个功能是,把这些资源文件中的文本内容抽取出来放在txt文件中。怎么实现?

如下,其中,ResourceFile 是一个抽象类,包含一个抽象函数 extract2txt()。PdfFile、PPTFile、WordFile 都继承 ResourceFile 类,并且重写了 extract2txt() 函数。在 ToolApplication中,我们可以利用多态特性,根据对象的实际类型,来决定执行哪个方法。

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

如果工具的功能不停的扩展,不仅要能抽取文本内容,还要支持压缩、提取文件元信息(文件名、大小、更新时间等)构建索引等一系列的功能,那如果我们继续按照上面的实现思路,就会存在这样几个问题:

  • 违背开闭原则,添加一个新的功能,所有类的代码都要修改;
  • 虽然功能增多,每个类的代码都不断膨胀,可读性和可维护性都变差了;
  • 把所有比较上层的业务逻辑都耦合到 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;
	}
}

这其中最关键的一点设计是,我们把抽取文本内容的操作,设计成了三个重载函数。函数重载是 Java、C++ 这类面向对象编程语言中常见的语法机制。所谓重载函数是指,在同一类中函数名相同、参数不同的一组函数。

不过,上面的代码时编译不过的,main函数中的extractor.extract2txt(resourceFile);会报错。

我们知道,多态时一种多态绑定,可以在运行时获取对象的实际类型,来运行实际类型对应的方法。而函数重载是一种静态绑定,在编译时并不能获取对象的实际类型,而是根据声明类型执行类型对应的方法。

在上面main函数中,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代码省略...
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) {
			resourceFile.accept(extractor);  // -----------
		}
	}
	
	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;
	}
}

在执行main函数的resourceFile.accept(extractor);时,根据多态特性,程序会调用实际类型的accept函数,比如PdfFile 的 accept 函数,而根据PdfFile 的 accept 函数中extractor.extract2txt(this);的this类型时PdfFile的,在编译的时候就确定了,所以会调用extractor 的 extract2txt(PdfFile pdfFile) 这个重载函数。这就是访问者模式的关键所在。

现在,如果要添加新的功能,比如压缩功能,根据不同的文件类型,使用不同的压缩算法来压缩资源文件,那我们该如何实现呢?我们需要实现一个类似 Extractor 类的新类 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 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;
	}
}

上面代码还存在一些问题,添加一个新的业务,还是需要修改每个资源文件类,违反了开闭原则。针对这个问题,我们抽象出来一个 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 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;
	}
}

对于这个场景,还可以使用工厂模式来设计和实现。

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;
	}
	//...
}
//...PPTFile/WordFile跟PdfFile代码结构类似,此处省略...
public interface Extractor {
	void extract2txt(ResourceFile resourceFile);
}

public class PdfExtractor implements Extractor {
	@Override
	public void extract2txt(ResourceFile resourceFile) {
	//...
	}
}

//...PPTExtractor/WordExtractor跟PdfExtractor代码结构类似,此处省略...

public class ExtractorFactory {
	private static final Map<ResourceFileType, Extractor> extractors = new HashMa
	
	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 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;
	}
}

当需要添加新的功能的时候,比如压缩资源文件,类似抽取文本内容功能的代码实现,我们只需要添加一个 Compressor 接口,PdfCompressor、PPTCompressor、WordCompressor 三个实现类,以及创建它们的 CompressorFactory 工厂类即可。唯一
需要修改的只有最上层的 ToolApplication 类。基本上符合“对扩展开放、对修改关闭”的设计原则。

对于资源文件处理工具这个例子,如果工具提供的功能并不是非常多,只有几个而已,推荐使用工厂模式的实现方式,毕竟代码更加清晰、易懂。相反,如果工具提供非常多的功能,比如有十几个,推荐使用访问者模式,因为访问者模式需要定义的类要比工厂模式的实现方式少很多,类太多也会影响到代码的可维护性。

重新来看访问者模式

访问者者模式的英文翻译是 Visitor Design Pattern。定义为:允许一个或者多个操作应用到一组对象上,解耦操作和对象本身。

对于访问者模式的代码实现,实际上,在上面例子中,经过层层重构之后的最终代码,就是标准的访问者模式的实现代码。
在这里插入图片描述
最后,我们再来看下,访问者模式的应用场景。

一般来说,访问者模式针对的是一组类型不同的对象(PdfFile、PPTFile、WordFile)。不过,尽管这组对象的类型是不同的,但是它们继承相同的父类(ResourceFile)或者实现相同的接口。在不同的应用场景下,我们需要对这组对象进行一系列不相关的业务操作(抽取文本、压缩等),但为了避免不断添加功能导致类(PdfFile、PPTFile、WordFile)不断膨胀,职责越来越不单一,以及避免频繁地添加功能导致的频繁代码修改,我们使用访问者模式,将对象与操作解耦,将这些业务操作抽离出来,定义在独立细分的访问者类(Extractor、Compressor)

总结

访问者模式允许一个或者多个操作应用到一组对象上,设计意图是解耦操作和对象本身,保持类职责单一、满足开闭原则以及应对代码的复杂性。

对于访问者模式,学习的主要难点在代码实现。而代码实现比较复杂的主要原因是,函数重载在大部分面向对象编程语言中是静态绑定的。也就是说,调用类的哪个重载函数,是在编译期间,由参数的声明类型决定的,而非运行时,根据参数的实际类型决定的

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值