Visitor 設計模式 | 實現與解說 | 單、雙分派語言 | Java APT

Visitor 設計模式 | 實現與解說 | 單、雙分派語言 | Java APT

Overview of Content

Visitor 是將資料結構 & 資料處理分離,是設計模式中最複雜的一個

Visitor 設計模式是一種行為型設計模式,它主要用於將操作(算法)與元素的結構分離開來,使得在不改變元素結構的前提下,可以定義新的操作。這種模式通常用於訪問一組相關的元素,並在不同的元素上執行不同的操作,而不影響元素結構。

本篇文章將深入探討 Visitor 設計模式的基本概念、使用場景、定義及 UML 圖,以及其優缺點。我們將透過實際案例展示 Visitor 模式的實現方式,同時介紹如何擴展 Visitor 模式以應對更複雜的需求。此外,我們還將討論 Single dispatch 和 Double dispatch 的概念,深入解釋這兩種分派方式在 Visitor 設計模式中的應用。

如果你對於如何有效地處理元素結構中的不同操作感到困惑,或者希望實現一個靈活且可擴展的操作系統,那麼本文將為你提供深入的解說和實用的示例。

寫文章分享不易,如有引用參考請詳註出處,如有指導、意見歡迎留言(如果覺得寫得好也請給我一些支持),感謝 😀


Visitor 使用場景

● 物件結構穩定(若結構不穩定,這個設計模式的結構就會常常被更動,甚至影響到已經實作的子類),但需要常在該物件上定義新的操作

● 假設一物件由多個物件聚合而成,這些物件都有一個類用來接收訪問者的存取,而訪問者是一個界面,對於實做這個界面,存取到物件結構中不同類型的元素做出不同的處理

解開接收者、訪問者的耦合性

● 對一物件結構中物件進行 不同 並 不相關 的操作,避免該物件被汙染,也不希望新增操作時修改到該類別

如果需求是:需要不改變封裝,並對於數據有不同操作模式後,就可以使用 Visitor 模式

這個模式也非常適合用於 大規模的重構項目(因為需求已經很清晰、變動性不大),但 如果項目的需求不夠穩定、清晰,那建議先不要使用這個模式


Visitor 定義 & Visitor UML

● Visitor 定義:封裝一些作用於某個數據結構中的元素,它可以再 不改變數據結構的前提之下,重新定義訪問這個結構的行為

也就說說 Visitor 設計的目的在於:解耦數據結構、數據訪問(或是操作)的兩個部份

graph LR 數據 --> visitor visitor -.-> 結構 visitor -.-> 訪問

● Visitor 角色介紹

角色功能
Visitor 界面定義須訪問的元素(定義哪些實體元素可被訪問);實體元素 ConcreteElement
ConcreteVisitor實做操作元素
Element 抽象抽象元素,定義抽象訪法讓子類實現 Visitor 界面的訪問
ConcreteElement具體元素,將自身傳入 Visitor 訪問的方法
ObjectStructure容納多個不同的類,可以返回不同的結構讓使用者使用(Option)

很少抽象化 ObjectStructure 角色,它算是具體的邏輯類,組裝多個不同類

● Visitor 為啥要依賴具體 ConcreteElement ?

其實也是可以不用,但是如果不依賴具體 ConcreteElement 在 ConcreteVisitor 實做時就需要用 instanceof 來判斷

但如果使用了 instanceof 來判斷據理實做類,那就無法善用語言「分派」的特性!(分派寫在下面的小節)

從這裡也就可以看出 Visitor 設計的模型不符合「依賴倒置」原則,同時也是在提醒使用這個設計的人,需求要足夠穩定再使用

Visitor 優缺點

● Visitor 設計的優點 :

A. 單一責任:個角色職責分離,符合單一責任原則

B. 對於 Visitor 的方法 擴充相當自由、靈活性高

C. 將操作與資料解偶

● Visitor 設計的缺點 :

A. 當具體元素改變實 (Element),需要修改的成本太高

C. 違反了依賴倒制原則,為了分別處理依賴了具體類別


Visitor 實現

以下實現一個:男孩、女孩看到同一個商品時,關注的點不同(不同的反應也就是對於相同商品不同的 Visitor)

Visitor 標準實現

A. Element 抽象:重點是有一個方法其變數依賴於 Visitor


abstract class MyTools constructor(val name: String, val price: Int) {
    
    // 依賴 Visitor
    abstract fun accept(visitor: Visitor)
}

B. ConcreteElement 類:實做類,也就是一個不修改的數據結構


class Robot constructor(name: String, price: Int) : MyTools(name, price) {

    fun doWork(): String {
        return "Working, learning, Server"
    }
    
    override fun accept(visitor: Visitor) {
        // 傳入自身
        visitor.visitor(this)
    }

}

class Cosmetic constructor(name: String, price: Int) : MyTools(name, price) {

    fun doWork(): String {
        return "Make face beauty"
    }
    
    override fun accept(visitor: Visitor) {
        // 傳入自身
        visitor.visitor(this)
    }

}

C. Visitor 界面:定義須訪問的元素

● 從 Visitor 的界面設計,我們可以發現,它不符合依賴倒置,這樣做好嗎?

Visitor 界面依賴實做(ConcreteElement)的類,也就是以下的 Robot, Cosmetic 類

但是!這也是 Visitor 設計在利用「單分派」的特性!


interface Visitor {

    // 定義須訪問的元素
    fun visitor(robot: Robot)

    // 定義須訪問的元素
    fun visitor(cosmetic: Cosmetic)

}

D. ConcreteVisitor 類:透過它來達到 同種結構不同的訪問方式(男女對於不同產品的看法)


// 關注點不同
class BoyVisitor : Visitor {

    override fun visitor(robot: Robot) {
        // Boy 關注 doWork
        println("I am Boy, focus Robot ${robot.name}, ${robot.doWork()}")
    }

    override fun visitor(cosmetic: Cosmetic) {
        // Boy 關注 price
        println("I am Boy, focus Cosmetic ${cosmetic.name}, ${cosmetic.price}")
    }

}

// 關注點不同
class GirlVisitor : Visitor {

    override fun visitor(robot: Robot) 
        // Girl 關注 price
        println("I am Girl, focus Robot ${robot.name}, ${robot.price}")
    }

    override fun visitor(cosmetic: Cosmetic) { 
        // Girl 關注 doWork
        println("I am Girl, focus Cosmetic ${cosmetic.name}, ${cosmetic.doWork()}")
    }

}

E. ObjectStructure 類:該類的目的是用來收集、遍歷所有的可被訪問者(資源),並且它也不需要知道訪問者是如何操作資料,就可以體現出「相同的資料,不同的實做」的特點


class ManagerIterator {

    private val ls = ArrayList<MyTools>()

    init {
        ls.add(Robot("多拉A夢", 300))
        ls.add(Robot("多拉美", 299))
        ls.add(Robot("佛朗基", 699))
        ls.add(Cosmetic("SKII", 333))
        ls.add(Cosmetic("Dr.Wu", 123))
    }

    // 遍歷所有的可訪問類
    fun accept(v: Visitor) {
        for (t in ls) {
            // 抽象方法,實作類不同會有不同結果 (這就是我們要的結果)
            t.accept(v)
        }
    }
}

● 測試 訪問者 實作:由使用者來決定要使用哪個「訪問者」,從結果我們可以看到,不同訪問者訪問相同資源會有不同的反應

從這裡我們可以看到,訪問者模式允許使用者透過不同的物件對元素(資料)做不同角度、方向的訪問


fun main() {
    ManagerIterator().run {
        accept(BoyVisitor())

        println("----------------------")

        accept(GirlVisitor())
    }
}

拓展 Visitor:抽象 Mutli Visitor 實作

● 多個訪問者的情況也是非常常見的,我們可以透過「繼承」 Visitor 界面,來達到拓展 Visitor 界面;如下圖 UML,每個訪問者可以再拓展不同界面

● 以下已會修改到的類來展示;現在我們在 根據功能 (總金額、購物明細) 來拓展兩個 Visitor

A. Visitor 拓展:根據功能繼承 Visitor 並拓展其功能

● 繼承 Visitor 界面,拓展總金額 getTotalCost 方法


interface ICostVisitor : Visitor {

    fun getTotalCost() : Int

}

● 繼承 Visitor 界面,拓展購物明細 showShoppingDetail 方法


interface IShoppingDetailVisitor : Visitor {

    fun showShoppingDetail()

}

B. ConcreteVisitor 類:由於上面我們拓展 Visitor 抽象,所以在這裡也需要相對應的實做,實做範例如下…

● 實做總金額 Visitor


class CostVisitorImpl : ICostVisitor {

    private var totalCost : Int = 0
    override fun getTotalCost(): Int {
        return totalCost
    }

    override fun visitor(robot: Robot) {
        totalCost += robot.price
    }

    override fun visitor(cosmetic: Cosmetic) {
        totalCost += cosmetic.price
    }

}

● 實做購物明細 Visitor


class ShoppingCarVisitorImpl : IShoppingDetailVisitor {

    private val str : StringBuffer = StringBuffer()

    override fun showShoppingDetail() {
        println("$str")
    }

    override fun visitor(robot: Robot) {
        str.append("${robot.name} : ${robot.price}").append("\n")
    }

    override fun visitor(cosmetic: Cosmetic) {
        str.append("${cosmetic.name} : ${cosmetic.price}").append("\n")
    }

}

● 修改到這邊就結束了,可以發現要拓展 Visitor 類是相當簡單的,並不需要修改之前已經實做好的類

● 但這似乎不滿足「開閉原則」?

不!這是符合開閉原則的,因為每多一個 Visitor 拓展類只需「新增」而不需要修改舊有的類型

● 使用拓展 Visitor 類的功能


fun main() {
    ManagerIterator().run {
        // 使用我們新拓展的 ShoppingCarVisitorImpl 訪問者,來訪問資源
        ShoppingCarVisitorImpl().run {
            accept(this)     // 這裡傳入的 this 是 ShoppingCarVisitorImpl

            showShoppingDetail()
        }

        println("--------------------------------------")
        
        // 使用我們新拓展的 CostVisitorImpl 訪問者,來訪問資源
        CostVisitorImpl().run {
            accept(this)    // 這裡傳入的 this 是 CostVisitorImpl

            println("total=(${getTotalCost()})")
        }
    }
}


Single, Double dispatch 單、雙分派

● 程式語言中,有分為「單分派, Single dispatch」、「雙分派Double dispatch」,這會與多型的概念有交互

● 單、雙分派定義:

● 雙分派:真正的執行操作決定於 請求者的種類、接收者的類型(由兩者共同決定);這種情況下,多態性是基於多個參數的類型

多出現在動態語言中

● 單分派:反知,單分派的執行則不是由這兩者決定;它指根據一個操作的一個參數的類型(稱為接收者)來選擇方法的過程;這種情況下,多態性是基於單一的參數類型

單、雙分派的判斷:Kotin 語言

● 我們可以從程式語言的實作中,測試出這個程式語言是支持「單分派」還是「雙分派」;像是 Java、Kotlin 就是支持單分派的程式語… 接下來我們 透過測試 Kotlin 語言來判斷 Kotlin 到底是屬於單分派還是雙分派

單分派的特點在於:實際執行方法的類型,是由「接收者」定義(這裡說的接收者就是宣告的類型)

A. 宣告抽象界面( interface)、實現類(class),等等測試會用「宣告界面」、「使用實體類」比較


interface People

class RichPeople : People

class PoorPeople : People

B. 抽象類:這裡使用 Overload 重載 buy 函數,等等用來判斷單、雙分派的結果


abstract class Shop {

    fun buy(people: People) {
        println("People buy something")
    }

    // 重載(overload)函數,並開放子類重載(Override)
    open fun buy(people: RichPeople) {
        println("RichPeople buy something")
    }

}

C. 繼承抽象 Shop 類,並 Override 覆寫 buy 函數,看看等等是否會被呼叫到


class BookShop: Shop() {
    // 覆寫函數
    override fun buy(people: RichPeople) {
        // 書出不同的資訊
        println("RichPeople buy book")
    }

}

● 測試 Kotlin 語言:透過宣告抽象(interface)、類型(Class)看看是否會影響到最終會抵達的 buy 函數… 請注意以下註釋


fun main() {
    
    // 宣告類型為 Shop,接收者類型為 Shop
    val shop : Shop = BookShop()

    // --------------------------------- 測試動態綁定 ---------------------------------
    // 這個是 buy 是呼叫到哪個方法呢?
    // 它呼叫到的是 BookShop#buy(RichPeople) 方法
    shop.buy(RichPeople())      // 請求者類型為 RichPeople


    // 這個是 buy 是呼叫到哪個方法呢?
    // 它會呼叫到抽象 Shop#buy(People) 方法
    val people : People = PoorPeople()     // 請求者類型為 People
    shop.buy(people)

}

從以下結果我們可以看到 Kotlin 這門語言是呼叫方法時的決定權是「是由宣告類型(接收者),而不是 由實作類型決定!」

我們可以看到 Kotlin 單以由請求者類型(接收者)決定函數的去向,所以 Kotlin 是單分派語言

  • 請觀察 shop.buy(RichPeople()) 語句,它的接收者類型為 RichPeople,所以就指向 RichPeople 類
  • 請觀察 val people : People = PoorPeople() 語句,它的接收者類型為 Prople,所以雖然使用了 PoorPeople 但實際上它是指向 People!!

Visitor 拓展單分派

● 從上面的實現我們可以知道 Java、Koltin 語言是 單分派(Single dispatch)的功能,在這邊我們再次加強單分派的功能特性,將 Visitor 改造,讓它「加強單分派功能」

A. Visitor 界面、ConcreteVisitor 實做:這裡是修改原先 Visitor 設計的關鍵,透過 overload、override 來補足設計的不足之處


// Visitor 讓其支持單分派特性,由外部請求者類型決定實作者
abstract class Shop2 {

    fun buy(people: People2) {
        println("People buy something")
    }

    // overload
    open fun buy(people: RichPeople2) {
        println("RichPeople buy something")
    }

}

// ConcreteVisitor
class BookShop2: Shop2() {
    
    // override 
    override fun buy(people: RichPeople2) {
        println("RichPeople buy book")
    }

}

B. Element、ConcreteElement:這部分與原先設計相同,讓 Element 依賴於 Visitor 抽象類(也就是讓以下範例的 People2 依賴於 Shop2)


// Element 界面
interface People2 {
    // 改為依賴抽象
    fun accept(shop: Shop2)

}

// ConcreteElement 實做
class RichPeople2 : People2 {
    override fun accept(shop: Shop2) {
        shop.buy(this)
    }
}

// ConcreteElement 實做
class PoorPeople2 : People2 {
    override fun accept(shop: Shop2) {
        shop.buy(this)
    }
}

● 測試修改後的 Visitor 設計:

從結果我們可以看到,透過修改後 Visitor 設計,可以讓單分派語言體現更多個可能性 😌(顯示出語言的細節,讓單分派由外部決定執行的真正類別)


fun main() {
    // 接收者類型定義為 Shop2(接收者 Visitor)
    val shop : Shop2 = BookShop2()

    // 動態判定:傳入類型為 RichPeople2(請求類 Element)
    shop.buy(RichPeople2())           

    // 靜態判定:傳入類行為 People2(請求類 Element)
    val people : People2 = RichPeople2()
    shop.buy(people)
}

● 透過 Visitor 設計 可以讓單分派語言擁有雙分派語言的特性,但是它只是 類似雙分派,並非真正的雙分派語言的判斷(因為語言的雙分派是語言自身實現的判斷機制,而 Visitor 則是在運用語言的層面上去添加的機制)


Java APT 分析

Visitor 模式的具體實現也應用在 Java APT(Annotaion Processing Tools) 框架之上,而 APT 框架的實現:有名的像是 ButterKnife、Dagger、Retrofit... 等等開源庫

● 這裡為 Java APT 技術做個簡單的介紹:

APT 技術其實是基於 Meta programming 的角度來開發,而這個 Meta programming 是 運作在編譯期間,透過編譯期間分析源碼中的註釋(Annotaion)來達到新類別的產生或是其他的功能!

Element 數據、ElementVisitor 訪問

● 我們先從 Element 界面入手,該界面功能是的含意是「表示語言級的程式元素」,包括套件 package、類別 class、方法 method... 等等

以下我們來看看這個 Element 界面中的幾個常見方法


// Element.java

public interface Element {
    
    ... 省略部份
    
    // 取得註解類
    <A extends Annotation> A getAnnotation(Class<A> annotationType);
    
    // 取得修飾符,像是 public, static, final... 等等
    Set<Modifier> getModifiers();
    
    // 把透過訪問者,來訪問數據結構
    <R, P> R accept(ElementVisitor<R, P> v, P p);
}

而在這裡,我們主要關注的是 accept 方法,這個方法是 Visitor 設計的入口:我們把它做個拆分理解,如下表…

Visitor 設計角色APT 中代表的類
數據的結構Element 界面
數據的訪問ElementVisitor 界面

從表中,我們可以分清晰的看到,APT 的設計就是明確的使用 Visitor 設計,它將源碼的結構透過 Element 界面來表達,而訪問源碼的方式使用 ElementVisitor 界面,清晰的分離了數據、以及對數據的操作!

● 接著,我們來看 ElementVisitor 界面,這個界面的主要功能是「編譯期操作源碼」,它的方法如下,從這些方法中我們可以發現幾件事

A. 已有的訪問者:APT 在設計時,有預計的幾個訪問方案,像是對於 Package 可使用是 PackageElement、對於類型可以使用 TypeElement... 等等類

B. 兼容變化:ElementVisitor#visitUnknown 的方法設計,是為了界面的穩定性,為未來拓展的 MetaData 做兼容變化

● 透過這個方法來達到遵守「開閉原則」的需求,每當有心的 MetaData 時,也不用去修改這個界面上的功能


// ElementVisitor.java

/**
 * R: 此訪客方法的傳回 (return) 類型
 * P: 此訪客的附加參數 (Params) 的類型
 */ 
public interface ElementVisitor<R, P> {

    /**
     * Visits an element.
     */
    R visit(Element e, P p);

    /**
     * A convenience method equivalent to {@code v.visit(e, null)}.
     */
    R visit(Element e);

    /**
     * Visits a package element.
     */
    R visitPackage(PackageElement e, P p);

    /**
     * Visits a type element.
     */
    R visitType(TypeElement e, P p);

    /**
     * Visits a variable element.
     */
    R visitVariable(VariableElement e, P p);

    /**
     * Visits an executable element.
     */
    R visitExecutable(ExecutableElement e, P p);

    /**
     * Visits a type parameter element.
     */
    R visitTypeParameter(TypeParameterElement e, P p);

    /**
     * Visits an unknown kind of element.
     * This can occur if the language evolves and new kinds
     * of elements are added to the {@code Element} hierarchy.
     */
    R visitUnknown(Element e, P p);
}

APT ElementVisitor 的拓展範例

● APT source code 中有提供幾個拓展範例,這邊我們就來看看它如何去拓展 ElementVisitor 元素(新增訪問者)

源碼中有一個 SimpleElementVisitor6 類,它繼承於 AbstractElementVisitor6,而 AbstractElementVisitor6 又實做了 ElementVisitor 界面

也就是說 SimpleElementVisitor6 是一個新的訪問程式 MetaData 的類別,它的部份程式如下:我們可以看出這個 Visitor 其實沒有做甚麽事情,單純返回一個預設值 DEFAULT_VALUE


// SimpleElementVisitor6.java

@SupportedSourceVersion(RELEASE_6)
public class SimpleElementVisitor6<R, P> extends AbstractElementVisitor6<R, P> {
    
    protected final R DEFAULT_VALUE;
    
    ... 省略部份
        
    protected R defaultAction(Element e, P p) {
        return DEFAULT_VALUE;
    }

    /**
     * {@inheritDoc} This implementation calls {@code defaultAction}.
     *
     * @param e {@inheritDoc}
     * @param p {@inheritDoc}
     * @return  the result of {@code defaultAction}
     */
    public R visitPackage(PackageElement e, P p) {
        return defaultAction(e, p);
    }

    /**
     * {@inheritDoc} This implementation calls {@code defaultAction}.
     *
     * @param e {@inheritDoc}
     * @param p {@inheritDoc}
     * @return  the result of {@code defaultAction}
     */
    public R visitType(TypeElement e, P p) {
        return defaultAction(e, p);
    }

    /**
     * {@inheritDoc} This implementation calls {@code defaultAction}.
     *
     * @param e {@inheritDoc}
     * @param p {@inheritDoc}
     * @return  the result of {@code defaultAction}
     */
    public R visitVariable(VariableElement e, P p) {
        return defaultAction(e, p);
    }

    /**
     * {@inheritDoc} This implementation calls {@code defaultAction}.
     *
     * @param e {@inheritDoc}
     * @param p {@inheritDoc}
     * @return  the result of {@code defaultAction}
     */
    public R visitExecutable(ExecutableElement e, P p) {
        return defaultAction(e, p);
    }

    /**
     * {@inheritDoc} This implementation calls {@code defaultAction}.
     *
     * @param e {@inheritDoc}
     * @param p {@inheritDoc}
     * @return  the result of {@code defaultAction}
     */
    public R visitTypeParameter(TypeParameterElement e, P p) {
        return defaultAction(e, p);
    }
}

● 接著,我們來看 ElementKindVisitor6 類別,它繼承於 SimpleElementVisitor6 類;在這個類中,它透過 TypeElement#getKind() 方法取得 MetaData,並拓展了類型(Type)的訪問


// ElementKindVisitor6.java

@SupportedSourceVersion(RELEASE_6)
public class ElementKindVisitor6<R, P>
                  extends SimpleElementVisitor6<R, P> {

    ... 省略部份

    @Override
    public R visitType(TypeElement e, P p) {
        ElementKind k = e.getKind();
        switch(k) {
        case ANNOTATION_TYPE:
            return visitTypeAsAnnotationType(e, p);

        case CLASS:
            return visitTypeAsClass(e, p);

        case ENUM:
            return visitTypeAsEnum(e, p);

        case INTERFACE:
            return visitTypeAsInterface(e, p);

        default:
            throw new AssertionError("Bad kind " + k + " for TypeElement" + e);
        }
    }

    public R visitTypeAsAnnotationType(TypeElement e, P p) {
        return defaultAction(e, p);
    }

    ... 省略部份

}

更多的物件導向設計

物件導向的設計基礎如下,如果是初學者或是不熟悉的各位,建議可以從這些基礎開始認識,打好基底才能走個更穩(在學習的時候也需要不斷回頭看)!

● 設計建模 2 大概念- UML 分類、使用

● 物件導向設計原則 – 6 大原則(一)

● 物件導向設計原則 – 6 大原則(二)

● 創建、行為、結構型設計 8 個比較 | 包裝模式 | 最佳實踐

創建模式 Creation Patterns

● 創建模式 PK

● 創建模式 - Creation Patterns:

結構模式 Structural Patterns

● 結構模式 PK

● 結構模式 - Structural Patterns:

結構模式專注於「物件之間的組成」,以形成更大的結構。這些模式可以幫助你確保當系統進行擴展或修改時,不會破壞其整體結構。例如,外觀模式、代理模式… 等等,詳細解說請點擊以下連結

● Bridge 橋接模式 | 解說實現 | 物件導向設計

● Decorate 裝飾模式 | 解說實現 | 物件導向設計

● Proxy 代理模式 | 解說實現 | 分析動態代理

● Iterator 迭代設計 | 解說實現 | 物件導向設計

● Facade 外觀、門面模式 | 解說實現 | 物件導向設計

● Adapter 設計模式 | 解說實現 | 物件導向設計

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

發表迴響