訪問者模式

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.

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息