访问者模式

Design Pattern: Visitor

要解决的问题

有一个数据结构有多种子数据结构聚合而成,须要在这些子数据结构分别进行不一样的操做,且有不少种不一样的操做类型。若是要在每一个数据结构里都分别定义对应的操做,会使得逻辑变得很复杂,并且当有新的操做类型时须要修改全部的类。编程

如图所示,咱们有两种 Element 类,为了在它们的持有者中实现两个操做 operate1 和 operate2,咱们须要在每一个 Element 里都实现操做的对应部分。若是这个时候咱们想要增长一种操做类型,那么咱们就必须修改每一个 Element 类。浏览器

假设咱们会常常变化操做的种类,那么咱们每次都要去修改全部的 Element 类, 这样会致使大量不相关的逻辑堆积在 Element 类中,最终致使代码变得难以维护。数据结构

结构

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

咱们发现每一个操做都会遍历全部的 Element 对象,这个逻辑是不变的,变化的只是遍历时要作的事情,因此咱们把要作的事情定义成一个抽象层次,经过一个 Visitor 类来实现要作的事的逻辑,而本来的类自己只须要接收一个 Visitor 对象而后遍历全部成员并应用 visitor 对象来完成对成员对象的操做。这样咱们就将变化的部分从整个结构中抽离了出来,若是咱们须要增长一种新的操做,只须要在实现一个新的 Visitor 类就能够了。this

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

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

多态与多路分发

面向对象编程一个最主要的概念就是类的继承,经过在类之间创建继承关系,咱们能够在须要一个父类声明的时候实际使用一个子类对象,若是这个子类对象复写了父类的方法,那么相同的调用在不一样的实际子类对象上就有了不一样的行为,这就是多态的概念。cdn

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:blog

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.

相关文章
相关标签/搜索
本站公众号
   欢迎关注本站公众号,获取更多信息