有一個數據結構有多種子數據結構聚合而成,須要在這些子數據結構分別進行不一樣的操做,且有不少種不一樣的操做類型。若是要在每一個數據結構裏都分別定義對應的操做,會使得邏輯變得很複雜,並且當有新的操做類型時須要修改全部的類。編程
如圖所示,咱們有兩種 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 模式的結構來實現。
by Orab.