重修设计模式-行为型-访问者模式

devtools/2024/10/25 5:00:12/

重修设计模式-行为型-访问者模式

Allows for one or more operation to be applied to a set of objects at runtime, decoupling the operations from the object structure.

允许一个或者多个操作应用到一组对象上,解耦操作和对象本身。

访问者模式(Visitor Pattern)通过将操作分离到独立的访问者类中,从而可以在不修改现有类层次结构的情况下增加新的操作。访问者模式比较难理解,下面通过一个例子来了解访问者模式的原理。

假设现在需要处理一批资源文件,它们的格式有三种:PDF、PPT、Word。现在需要开发一个工具来处理这批资源文件,比如提取资源文件中的文本放到 txt 文件中。需求非常简单,稍微分析依稀就可以写出如下代码:

//资源文件抽象
abstract class ResourceFile(val filePath: String) {abstract fun extract2txt()
}class PDFFile(filePath: String): ResourceFile(filePath) {override fun extract2txt() {println("提取PDF文件中文字信息...")}
}class PPTFile(filePath: String): ResourceFile(filePath) {override fun extract2txt() {println("提取PPT文件中文字信息...")}
}class WordFile(filePath: String): ResourceFile(filePath) {override fun extract2txt() {println("提取Word文件中文字信息...")}
}//调用:
fun main() {val files = mutableListOf(PDFFile("a.pdf"), PPTFile("b.ppt"), WordFile("c.word"))files.forEach { file ->file.extract2txt()  //通过多态特性,调用运行时对象的具体方法}
}

若需求到此为止,这样写并没有太大问题,但随着需求的扩展,如还需要支持压缩、提取文件描述信息等,问题也就随之暴露:

  • 每添加一个新功能,都会改动到所有类的代码,违反开闭原则
  • 上层业务都耦合到具体的类(PDFFile、PPTFile、WordFile)中,导致类越来越膨胀,违反单一职责

针对上述问题,往往解决方式都是拆分解耦,把业务操作跟具体的数据结构解耦,设计成独立的类。那么上述代码,要如何拆分解耦呢?

首先 ResourceFile 只表示资源文件的数据结构,需要将业务操作(如 extract2txt 方法)拆分出去。且同一系列的操作最好放到同一个类中。这里可以创建 Extractor 类,专门负责不同资源文件的文字提取操作,代码重构完如下:

//数据结构
abstract class ResourceFile(val filePath: String) {
}class PDFFile(filePath: String): ResourceFile(filePath) {
}class PPTFile(filePath: String): ResourceFile(filePath) {
}class WordFile(filePath: String): ResourceFile(filePath) {
}//业务操作:提取文字
class Extractor() {fun extract2txt(file: PDFFile) {println("提取PDF文件中文字信息...")}fun extract2txt(file: PPTFile) {println("提取PPT文件中文字信息...")}fun extract2txt(file: WordFile) {println("提取Word文件中文字信息...")}
}//调用:
fun main() {val files = mutableListOf(PDFFile("a.pdf"), PPTFile("b.ppt"), WordFile("c.word"))val extractor = Extractor()files.forEach { file ->//file.extract2txt()  //通过多态特性,调用具体运行时对象的具体方法extractor.extract2txt(file) //编译报错,并不能通过重载,调用到对象运行时的具体方法}
}

重构后数据结构和业务操作解耦了,如果添加新的业务操作也只需要增添新的业务操作类即可。想法很美好,但在 Java 中这样做是行不通的,在调用 extract2txt 方法时会报编译错误:找不到参数类型为 ResourceFileextract2txt 方法。
在这里插入图片描述
这是因为,Java 语言的语法,只支持 Single Dispatch(单分派)机制。

什么是 Single Dispatch 和 Double Dispatch?

Single Dispatch(单分派) 和 Double Dispatch(双分派) 跟多态函数重载直接相关。多态是一种动态绑定,可以在运行时获取对象的实际类型,来运行实际类型对应的方法。而函数重载是一种静态绑定,在编译时并不能获取对象的实际类型,而是根据声明类型执行声明类型对应的方法。多态表示执行哪个对象的方法,重载表示执行对象的哪个方法。

Single Dispatch 之所以称为“Single”,是因为执行哪个对象的哪个方法,只跟**“对象”的运行时类型有关。Double Dispatch 之所以称为“Double”,是因为执行哪个对象的哪个方法,跟“对象”“方法参数”**两者的运行时类型有关。

从多态角度来看,Single DispatchDouble Dispatch 是相同的:

  • 执行哪个对象的方法,都根据对象的运行时类型来决定。

从函数重载来看,Single DispatchDouble Dispatch 则不同:

  • Single Dispatch:执行对象的哪个方法,根据方法参数的编译时类型来决定。
  • Double Dispatch:执行对象的哪个方法,根据方法参数的运行时类型来决定。

举个例子:

open class Parent() {open fun f() {println("Parent's f()")}
}class Child(): Parent() {override fun f() {println("Child's f()")}
}class SingleDispatch() {fun overloadFunction(p: Parent) {println("SingleDispatchDemo's Parent type function.")}fun overloadFunction(p: Child) {println("SingleDispatchDemo's Child type function.")}
}fun main() {//多态val p: Parent = Child()p.f()   //执行哪个对象的方法,由对象的实际类型决定(运行时决定)//重载val s = SingleDispatch()s.overloadFunction(p)   //执行对象的哪个方法,由参数对象的声明类型决定(编译时决定)
}//执行结果:
Child's f()
SingleDispatch's Parent type function.

由于 Kotlin 是单分派机制,所以重载代码那里匹配的是 SingleDispatch 的 overloadFunction(p: Parent) 函数,也就是根据 p 的声明类型来决定匹配哪个重载函数,而不是运行时真实类型。

当前主流的面向对象编程语言(比如,Java、C++、C#)都只支持 Single Dispatch,不支持 Double Dispatch。

中介者模式实现

再回到最初的资源文件处理例子,如果语言支持 Double Dispatch,那么重构后的代码直接可以跑通,就无需中介者模式了。但主流编程语言大多还是 Single Dispatch 机制,这就需要中介者模式来处理运行时的函数重载问题。

运行时重载在语言层面时行不通的,那么能否将函数重载问题转换为多态问题呢?当然是可以的,首先在调用处我们无法直接使用声明类型 ResourceFile 来调用 Extractor 中对应子类型的参数方法(单分派语言的限制),但 ResourceFile 子类的内部是知道自己具体类型的,再根据通过多态特性,运行时是知道 ResourceFile 具体类型的,那么只要将访问 Extractor 的位置由调用处移动到 PDFFile、PPTFile 和 WordFile 内部不就可以了,下面来验证一下这个想法是否可行:

//数据结构
abstract class ResourceFile(val filePath: String) {abstract fun accept(extractor: Extractor)
}class PDFFile(filePath: String): ResourceFile(filePath) {override fun accept(extractor: Extractor) {extractor.extract2txt(this)}
}class PPTFile(filePath: String): ResourceFile(filePath) {override fun accept(extractor: Extractor) {extractor.extract2txt(this)}
}class WordFile(filePath: String): ResourceFile(filePath) {override fun accept(extractor: Extractor) {extractor.extract2txt(this)}
}//业务操作:提取文字
class Extractor() {fun extract2txt(file: PDFFile) {println("提取PDF文件中文字信息...")}fun extract2txt(file: PPTFile) {println("提取PPT文件中文字信息...")}fun extract2txt(file: WordFile) {println("提取Word文件中文字信息...")}
}fun main() {val files = mutableListOf(PDFFile("a.pdf"), PPTFile("b.ppt"), WordFile("c.word"))val extractor = Extractor()files.forEach { file ->//file.extract2txt()  //通过多态特性,调用具体运行时对象的具体方法//extractor.extract2txt(file) //编译报错,并不能通过重载,调用到对象运行时的具体方法file.accept(extractor)  //将访问操作放到数据结构内部,根据多态特性得到真实类型再去调用}
}

编译通过,可以看到通过将“运行时执行对象的哪个方法”问题,转换为了 “运行时执行哪个对象的方法”,从而绕过了单分派机制语言的限制,这虽然不是完整的访问者模式,但已包含访问者模式的核心原理了。

下面再根据面向抽象的编程原则,将所有业务操作进一步抽象成一个接口,从而方便业务的灵活扩张,最终代码实现如下:

//数据结构
abstract class ResourceFile(val filePath: String) {abstract fun accept(visitor: Visitor)
}class PDFFile(filePath: String): ResourceFile(filePath) {override fun accept(visitor: Visitor) {visitor.visit(this)}
}class PPTFile(filePath: String): ResourceFile(filePath) {override fun accept(visitor: Visitor) {visitor.visit(this)}
}class WordFile(filePath: String): ResourceFile(filePath) {override fun accept(visitor: Visitor) {visitor.visit(this)}
}//抽象业务操作:访问者
interface Visitor {fun visit(file: PDFFile)fun visit(file: PPTFile)fun visit(file: WordFile)
}//具体业务操作1:提取文字
class ExtractorVisitor(): Visitor {override fun visit(file: PDFFile) {println("提取PDF文件中文字信息...")}override fun visit(file: PPTFile) {println("提取PPT文件中文字信息...")}override fun visit(file: WordFile) {println("提取Word文件中文字信息...")}
}//具体业务操作2:压缩文件
class CompressorVisitor(): Visitor {override fun visit(file: PDFFile) {println("压缩PDF文件内容...")}override fun visit(file: PPTFile) {println("压缩PPT文件内容...")}override fun visit(file: WordFile) {println("压缩Word文件内容...")}
}fun main() {val files: MutableList<ResourceFile> = mutableListOf(PDFFile("a.pdf"), PPTFile("b.ppt"), WordFile("c.word"))val extractor = ExtractorVisitor()val compressor = CompressorVisitor()files.forEach { file: ResourceFile ->file.accept(extractor)  //业务操作1file.accept(compressor) //业务操作2}
}

这就是完整的访问者模式,角色定义如下:

  1. Visitor(抽象访问者):声明访问者可以访问哪些元素,具体到程序中就是 visit 方法的参数类型,定义哪些对象是可以被访问的。
  2. ConcreteVisitor(具体访问者):实现Visitor接口中的各个访问操作,使每个操作可以访问并处理被访问对象中的具体类。
  3. Element(抽象元素):声明可以接受哪些类型的访问者方法,通常为accept
  4. ConcreteElement(具体元素)类:实现 Element 接口,并具体实现accept方法,以便在调用该方法时能够接受访问者的访问。
  5. ObjectStructure(对象结构)类:元素产生者,一般容纳在多个不同类、不同接口的容器,如List、Set、Map等,一般很少抽象出这个角色。

访问者通用类图如下:
在这里插入图片描述

访问者应用场景

访问者模式较难理解,使用时可能会导致代码的可读性、可维护性变差,所以,访问者模式在实际的软件开发中很少被用到。如果有以下场景,可以考虑访问者模式

  • 对复杂对象结构(如多类型的对象集合)中的所有元素执行某些操作,通过访问者为多个目标类提供相同操作的变体,从而在属于不同类的一组对象上执行同一操作。
  • 对象结构相对稳定,但经常需要在此对象结构上定义新的操作。
  • 需要对对象结构中的对象进行复杂的操作时,而这些操作又不适合定义在对象的类中。

总结

总的来说,访问者模式是一种强大的设计模式,它允许在不修改对象结构的情况下增加新的操作,但在使用时需要权衡其带来的复杂性和性能开销。


http://www.ppmy.cn/devtools/128593.html

相关文章

MATLAB中head函数用法

目录 语法 说明 示例 显示矩阵的前八行 显示表的前三行 返回表的前八行 head函数的功能是获取数组或表的顶行。 语法 head(A) head(A,k) B head(___) 说明 head(A) 在命令行窗口中显示数组、表或时间表 A 的前八行&#xff0c;但不存储值。 head(A,k) 显示 A 的前 k …

人工智能技术的应用前景及其对生活和工作方式的影响

人工智能技术的应用前景及其对生活和工作方式的影响 随着人工智能&#xff08;AI&#xff09;技术的迅猛发展&#xff0c;其在各个领域的应用正日益深入&#xff0c;深刻改变着我们的生活和工作方式。本文将系统地探讨人工智能的历史、现状、未来应用前景&#xff0c;以及其对个…

Dockerfile 中 Expose 命令的作用

Dockerfile 中 Expose 命令的作用 格式是&#xff1a;EXPOSE <端口1> [<端口2>...] 例如&#xff1a; EXPOSE 8080 8081 8082 特别注意&#xff1a; EXPOSE 指令是声明容器运行时提供服务的端口&#xff0c;请注意这只是一个声明&#xff0c;并没有实际作用&am…

前端使用xlsx和file-saver自定义导出excel表格,无需写页面直接导出数据。末尾有一个插件方式使用

建议把代码封装成一个函数&#xff0c;这样就不用每个页面都写了&#xff0c;直接调用就好了。 <template><div><h1>导出数据为 Excel</h1><button click"exportToExcel(dynamicData, [name, age, city], [姓名, 年龄, 城市],某某文件)"…

pytorch scatter_ 函数介绍

scatter_ 是 PyTorch 中的一个原地操作函数&#xff0c;用于在给定的索引处将某些值填充到张量的指定维度中。它的常见用途之一是将类别标签转换为 one-hot 编码&#xff0c;不过它也适用于其他场景&#xff0c;如在特定索引处更新张量的值。 scatter_ 函数的签名如下&#xf…

Facebook的AI驱动发展:人工智能如何改变社交体验

个性化内容推荐 Facebook利用AI算法分析用户的行为数据&#xff0c;包括点赞、评论、分享和浏览历史。这些数据使得平台能够深入了解用户的兴趣和偏好&#xff0c;从而提供个性化的内容推荐。例如&#xff0c;用户在浏览动态时&#xff0c;AI系统会根据用户的互动历史&#xf…

短视频去水印小程序流量主最新接口带配音功能

短视频去水印小程序最新版包更新接口 支持对接流量主盈利 支持各大短视频平台 如: 抖音、快手、等 可提一键取视频文案、可一键分析主页视频链接地址工具 新增&#xff1a;带配音功能&#xff0c;文案提取功能&#xff0c;独立后台&#xff0c;可以设置卡密&#xff0c;后台…

深入探究安卓 Binder 机制及其应用

在安卓开发的广袤领域中&#xff0c;Binder 机制宛如一座坚固的桥梁&#xff0c;连接着不同进程间的通信。理解 Binder 机制对于安卓开发者而言&#xff0c;是掌握系统底层原理、优化应用性能的关键。 首先&#xff0c;让我们深入剖析 Binder 机制的核心原理。Binder 本质上是…