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

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

Overview of Content

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

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

本篇文章將深入探討 Visitor 設計模式的基本概念、使用場景、定義及 UML 圖,以及其優缺點。我們將透過實際案例展示 Visitor 模式的實現方式,同時介紹如何擴展 Visitor 模式以應對更複雜的需求。此外,我們還將討論 Single dispatchDouble 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

其實也是可以不用,但是如果不依賴具體 ConcreteElementConcreteVisitor 實做時就需要用 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 語言

● 我們可以從程式語言的實作中,測試出這個程式語言是支持「單分派」還是「雙分派」;像是 JavaKotlin 就是支持單分派的程式語… 接下來我們 透過測試 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 拓展單分派

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

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


// 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. ElementConcreteElement:這部分與原先設計相同,讓 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 框架的實現:有名的像是 ButterKnifeDaggerRetrofit... 等等開源庫

● 這裡為 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?

發表迴響