重修设计模式-行为型-访问者模式
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 方法时会报编译错误:找不到参数类型为 ResourceFile
的 extract2txt
方法。
这是因为,Java 语言的语法,只支持 Single Dispatch(单分派)机制。
什么是 Single Dispatch 和 Double Dispatch?
Single Dispatch
(单分派) 和 Double Dispatch
(双分派) 跟多态 和函数重载直接相关。多态是一种动态绑定,可以在运行时获取对象的实际类型,来运行实际类型对应的方法。而函数重载是一种静态绑定,在编译时并不能获取对象的实际类型,而是根据声明类型执行声明类型对应的方法。多态表示执行哪个对象的方法,重载表示执行对象的哪个方法。
Single Dispatch
之所以称为"Single",是因为执行哪个对象的哪个方法,只跟**"对象"的运行时类型有关。Double Dispatch
之所以称为"Double",是因为执行哪个对象的哪个方法,跟 "对象"和"方法参数"**两者的运行时类型有关。
从多态角度来看,Single Dispatch
和 Double Dispatch
是相同的:
- 执行哪个对象的方法,都根据对象的运行时类型来决定。
从函数重载来看,Single Dispatch
和 Double 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
}
}
这就是完整的访问者模式,角色定义如下:
Visitor
(抽象访问者):声明访问者可以访问哪些元素,具体到程序中就是visit
方法的参数类型,定义哪些对象是可以被访问的。ConcreteVisitor
(具体访问者):实现Visitor接口中的各个访问操作,使每个操作可以访问并处理被访问对象中的具体类。Element
(抽象元素):声明可以接受哪些类型的访问者方法,通常为accept
。ConcreteElement
(具体元素)类:实现 Element 接口,并具体实现accept
方法,以便在调用该方法时能够接受访问者的访问。ObjectStructure
(对象结构)类:元素产生者,一般容纳在多个不同类、不同接口的容器,如List、Set、Map等,一般很少抽象出这个角色。
访问者通用类图如下:
访问者应用场景
访问者模式较难理解,使用时可能会导致代码的可读性、可维护性变差,所以,访问者模式在实际的软件开发中很少被用到。如果有以下场景,可以考虑访问者模式:
- 对复杂对象结构(如多类型的对象集合)中的所有元素执行某些操作,通过访问者为多个目标类提供相同操作的变体,从而在属于不同类的一组对象上执行同一操作。
- 对象结构相对稳定,但经常需要在此对象结构上定义新的操作。
- 需要对对象结构中的对象进行复杂的操作时,而这些操作又不适合定义在对象的类中。
总结
总的来说,访问者模式是一种强大的设计模式,它允许在不修改对象结构的情况下增加新的操作,但在使用时需要权衡其带来的复杂性和性能开销。