阅读 230

访问者模式

Design Pattern: Visitor

要解决的问题

有一个数据结构有多种子数据结构聚合而成,需要在这些子数据结构分别进行不同的操作,且有很多种不同的操作类型。如果要在每个数据结构里都分别定义对应的操作,会使得逻辑变得很复杂,而且当有新的操作类型时需要修改所有的类。

如图所示,我们有两种 Element 类,为了在它们的持有者中实现两个操作 operate1 和 operate2,我们需要在每个 Element 里都实现操作的对应部分。如果这个时候我们想要增加一种操作类型,那么我们就必须修改每个 Element 类。

假设我们会经常变化操作的种类,那么我们每次都要去修改所有的 Element 类, 这样会导致大量不相关的逻辑堆积在 Element 类中,最终导致代码变得难以维护。

结构

为了解决这个问题,我们可以尝试抽离变化的部分,在上述的例子中,变化的部分是具体的操作,那我们就把操作部分的逻辑抽象出来。

我们发现每个操作都会遍历所有的 Element 对象,这个逻辑是不变的,变化的只是遍历时要做的事情,所以我们把要做的事情定义成一个抽象层次,通过一个 Visitor 类来实现要做的事的逻辑,而原本的类本身只需要接收一个 Visitor 对象然后遍历所有成员并应用 visitor 对象来完成对成员对象的操作。这样我们就将变化的部分从整个结构中抽离了出来,如果我们需要增加一种新的操作,只需要在实现一个新的 Visitor 类就可以了。

以上就是 Visitor 模式要处理的问题,通过一个观察者将实际的处理逻辑从数据结构类中抽离出来,这样每个逻辑都完整的呈现在一个 Visitor 类中,而数据结构类也可以保持稳定的结构,不会因为加入过多的逻辑而变得难以维护。一个完整的 Visitor 模式的结构如下图所示:

和我们上面的结构相比,实际的 Visitor 模式有一些变化:调用 Visitor 的逻辑并不放在顶层类中,而是在每个 Element 类中定义了一个 accept 方法,顶层类只是依次调用 Element 的 accept 方法,而由 Element 类本身来调用 Visitor。为什么要这样做呢?这就涉及到面向对象编程中多态相关的概念。

多态与多路分发

面向对象编程一个最主要的概念就是类的继承,通过在类之间建立继承关系,我们可以在需要一个父类声明的时候实际使用一个子类对象,如果这个子类对象复写了父类的方法,那么相同的调用在不同的实际子类对象上就有了不同的行为,这就是多态的概念。

open class Source1

class Source2 : Source1()

open class Target1 {

    open fun dispatch(source1: Source1) {
        println("Dispatch Target1 from Source1")
    }

    open fun dispatch(source2: Source2) {
        println("Dispatch Target1 from Source2")
    }
}

class Target2 : Target1() {

    override fun dispatch(source1: Source1) {
        println("Dispatch Target2 from Source1")
    }

    override fun dispatch(source2: Source2) {
        println("Dispatch Target2 from Source2")
    }
}
复制代码

我们实现了一个简单的继承关系,Target2 类继承了 Target1 类,这样如果我们声明一个 Target1 的变量,并调用 dispatch 方法,通过给这个声明的变量赋值不同的实际对象,就会有不一样的行为:

var target: Target1 = Target1()
target.dispatch(Source1())
target = Target2()
target.dispatch(Source1())
复制代码

Output:

Dispatch Target1 from Source1
Dispatch Target2 from Source1
复制代码

我们看到具体调用父类还是子类的方法是在运行是动态决定的,这称为行为的动态分发。但是在一般的面向对象语言中,这种动态分发只适用于调用者,而不适用与参数:

val source: Source1 = Source2()
Target1().dispatch(source)
复制代码

Output:

Dispatch Target1 from Source1
复制代码

我们看到对于传入的参数,系统并没有在运行时通过实际的参数类型来决定应该调用哪个方法,而只是根据声明时的参数类型来决定调用方法。

因此我们说一般的面向对象语言都是单路分发的,即只有调用者有多态的行为而参数没有。如何实现调用者和参数都可以动态分发呢?我们需要改变一下代码的结构:

open class Source1 {

    open fun connect(target1: Target1) {
        println("Dispatch Target1 from Source1")
    }

    open fun connect(target2: Target2) {
        println("Dispatch Target2 from Source1")
    }
}

class Source2 : Source1() {

    override fun connect(target1: Target1) {
        println("Dispatch Target1 from Source2")
    }

    override fun connect(target2: Target2) {
        println("Dispatch Target2 from Source2")
    }
}

open class Target1 {

    open fun dispatch(source1: Source1) {
        source1.connect(this)
    }
}

class Target2 : Target1() {

    override fun dispatch(source1: Source1) {
        source1.connect(this)
    }
}
复制代码

这样我们相当于让参数也成为了调用者,通过两次的调用行为来模拟实现了二路分发。如果想实现多个参数的动态分发,可以按照这个思路继续扩展,让每个参数都有机会成为一次调用者即可。实际的调用如下:

val source: Source1 = Source2()
Target1().dispatch(source)
复制代码

Output:

Dispatch Target1 from Source2
复制代码

我们可以发现,这就是 Visitor 和我们初版方案的不同之处。

总结

用途

Visitor 模式一般会用在编译器处理语法树或者 Web 浏览器解析 DOM 树的场景中。而如果代码需要实现多路分发的逻辑,也可以按照 visitor 模式的结构来实现。

优点

  • 可以很方便的添加新的操作类型 (即新的 Visitor)
  • 将相关的操作聚集到了一起,并隔离了不相关的逻辑
  • 可以遍历访问不同的类型(相比于 Iterator 只能访问相同的类型,但是代价是需要预先就确定会有哪些类型)
  • 可以在遍历过程中记录状态

缺点

  • 一旦需要添加新类型就要改动大量的类
  • 打破了封装

by Orab.