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

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

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 文件中。需求非常简单,稍微分析依稀就可以写出如下代码:

kotlin 复制代码
//资源文件抽象
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 类,专门负责不同资源文件的文字提取操作,代码重构完如下:

kotlin 复制代码
//数据结构
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:执行对象的哪个方法,根据方法参数的运行时类型来决定。

举个例子:

kotlin 复制代码
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 内部不就可以了,下面来验证一下这个想法是否可行:

kotlin 复制代码
//数据结构
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)  //将访问操作放到数据结构内部,根据多态特性得到真实类型再去调用
    }
}

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

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

kotlin 复制代码
//数据结构
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)  //业务操作1
        file.accept(compressor) //业务操作2
    }
}

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

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

访问者通用类图如下:

访问者应用场景

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

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

总结

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

相关推荐
卡尔特斯4 小时前
Android Kotlin 项目代理配置【详细步骤(可选)】
android·java·kotlin
白鲸开源4 小时前
Ubuntu 22 下 DolphinScheduler 3.x 伪集群部署实录
java·ubuntu·开源
ytadpole4 小时前
Java 25 新特性 更简洁、更高效、更现代
java·后端
纪莫4 小时前
A公司一面:类加载的过程是怎么样的? 双亲委派的优点和缺点? 产生fullGC的情况有哪些? spring的动态代理有哪些?区别是什么? 如何排查CPU使用率过高?
java·java面试⑧股
JavaGuide5 小时前
JDK 25(长期支持版) 发布,新特性解读!
java·后端
用户3721574261355 小时前
Java 轻松批量替换 Word 文档文字内容
java
白鲸开源5 小时前
教你数分钟内创建并运行一个 DolphinScheduler Workflow!
java
晨米酱5 小时前
JavaScript 中"对象即函数"设计模式
前端·设计模式
Java中文社群6 小时前
有点意思!Java8后最有用新特性排行榜!
java·后端·面试
代码匠心6 小时前
从零开始学Flink:数据源
java·大数据·后端·flink