程序员阿沛
发布于 2026-06-27 / 0 阅读
0
0

面向对象和设计模式二十二访问者模式如何解耦操作和对象

面向对象和设计模式(二十二) 访问者模式——如何解耦操作和对象

访问者模式的定义

访问者模式是一种用来解耦操作和对象本身的设计模式,它将对象中复杂的操作从对象本身分离处理放到其他的访问者对象中,以满足类职责单一、开闭原则并应对代码复杂性。

访问者模式的演变和使用场景

假设我们需要解析一批文件,将这些文件的内容抽取到txt中,这些文件类型包括 PDF 和 Word。我们可以如何实现呢?

实现这个功能不难,下面我们提供一种实现方式,并以此为基础演变为访问者模式的实现方式,以及说明为什么需要这样做。

其中,ResourceFile 是一个抽象类,包含一个抽象函数 extract2txt()。PdfFile、WordFile 都继承
ResourceFile 类,并重写 extract2txt() 函数。

代码实现如下:

<?php

abstract class ResourceFile{ protected $fp; // 要解析的文件路径
public function __construct($fp){ $this->fp = $fp; }
abstract public function extract2txt();}
class PdfFile extends ResourceFile{ public function extract2txt(){ // 具体实现 }}

class WordFile extends ResourceFile{ public function extract2txt(){ // … }}
$pdf = new PdfFile(“./1.pdf”);$pdf->extract2txt();$word = new WordFile(“./1.docx”);$word->extract2txt();

如果工具类ResourceFile的功能需要不断扩展,除了提取文本到txt,还需要有压缩、提取文件元信息如
文件名、大小、更新时间等等,以及其他杂七杂八的功能,那么我们沿用上面的代码就会有以下问题:

a. 新加一个功能所有子类都需要添加新方法,违背开闭原则 且 导致类的可维护性变差;

b. 压缩和提取文本对于 ResourceFile
而言是一种比较上层的业务逻辑,而非它自己对象性质上的逻辑,将这些上层逻辑耦合到PdfFile、WordFile 类中,导致它们的职责不够单一;

我们的解决方案就是将这些杂乱的业务操作和原有对象解耦,将业务操作放到独立的类中。重构之后代码如下:

<?php
class ResourceConstant{    const FILE_TYPE_PDF = 'pdf';    const FILE_TYPE_WORD = 'word';}

abstract class ResourceFile{ protected $fp; // 要解析的文件路径
public function __construct($fp){ $this->fp = $fp; }
abstract public function getType();
public function execute(Visitor $visitor){ $visitor->execute($this); }}
class PdfFile extends ResourceFile{ public function getType(){ return ResourceConstant::FILE_TYPE_PDF; }}

class WordFile extends ResourceFile{ public function getType(){ return ResourceConstant::FILE_TYPE_WORD; }}
/** * @property ResourceFile $resourceFile */abstract class Visitor{ public function execute(ResourceFile $resourceFile){ $method = $this->getExecuteMethod($resourceFile); call_user_func_array([$this, $method], [$resourceFile]); // 意思是调用 $this->$method($resourceFile) }
protected function getExecuteMethod(ResourceFile $resourceFile){ return $this->executeMap($resourceFile->getType()); }
abstract public function executeMap($fileType);}
// 负责解析为txt逻辑的类class Extractor extends Visitor{ public function executeMap($fileType){ return [ ResourceConstant::FILE_TYPE_PDF => ‘extractPdf2txt’, ResourceConstant::FILE_TYPE_WORD => ‘extractWord2txt’, ]; }
public function extractPdf2txt(ResourceFile $resourceFile){ // 省略具体逻辑 … }
public function extractWord2txt(ResourceFile $resourceFile){ // 省略具体逻辑 … }}
// 负责压缩逻辑的类class Compressor extends Visitor{ public function executeMap($fileType){ return [ ResourceConstant::FILE_TYPE_PDF => ‘compressorPdf’, ResourceConstant::FILE_TYPE_WORD => ‘compressorWord’, ]; }
public function compressorPdf(ResourceFile $resourceFile){ // 省略具体逻辑 … }
public function compressorWord(ResourceFile $resourceFile){ // 省略具体逻辑 … }}

// 应用层代码$pdf = new PdfFile(“./1.pdf”);
// 如果我要解压$compressor = new Compressor();$pdf->execute($compressor);
// 如果我要提取文本到txt$extractor = new Extractor();$pdf->execute($extractor);

重构成这样的好处是,当我们以后扩展多一种 获取文件元信息 的能力的时候,就不需要再在每个 ResourceFile 类中添加方法,而是直接添加多一个
MetaDataExtractor 类继承自 Visitor 即可,保持了 ResourceFile 类的职责单一、维持了开闭原则 和
ResourceFile 类的可维护性。

这里提一个问题,假如我扩展的不是一种行为,而是一种文件类型那会怎么样?

例如,我现在要新增的不是 获取文件元信息 的能力,而是新增一种 PPTFile 类,要对PPT也能提取txt文本 以及 对PPT
也能压缩,这就意味着需要在每个 Visitor 子类中都添加方法,这不还是会破坏开闭原则么?

那此时就需要我们衡量业务的哪个维度扩展的频率比较高。如果业务上会更频繁的新增文件类型那么就直接使用继承和多态特性,在每个 ResourceFile
的子类中实现具体的行为;如果业务上更倾向于频繁的扩展文件的行为功能,而很长时间才增加一种文件类型,那么访问者模式此时就派得上用场。

访问者模式的类图

总结和回顾

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

对了,最后再提一点,上面的代码可以再进一步优化,添加一个 ResourceFileFactory 类,这个类的作用是根据 文件后缀 创建对应的
ResourceFile 对象,这样在应用层的调用上就更加的方便了。

$filePathList = ['1.pdf', '2.docx', '3.pdf'];$compressor = new Compressor();$extractor = new Extractor();foreach($filePathList as $fp){    $resourceFile = ResourceFileFactory::getInstance($fp);    $resourceFile->execute($compressor);        // 压缩    $resourceFile->execute($extractor); // 提取文本}

至此,面向对象和设计模式系列告一段落,下一个系列想和大家分享一下多路复用、IO模型和并发的艺术,希望阿沛的内容能帮助到大家~


评论