意圖(Intent)#

表示一個作用於物件結構中各元素的操作。Visitor 讓你可以在不修改元素類別的前提下,定義作用於這些元素的新操作。

動機(Motivation)#

考慮一個編譯器,以抽象語法樹(AST)表示程式。編譯器需要對 AST 執行各種操作:型別檢查、程式碼最佳化、流程分析、程式碼生成、格式化輸出、計算各種指標等。

問題在於:將這些操作分散到各個節點類別中,會使系統難以理解、維護和修改——型別檢查的程式碼會與格式化輸出的程式碼混雜在一起。而且每新增一個操作,通常需要重新編譯所有節點類別。

解法是將相關操作包裝在一個獨立的物件中,稱為 visitor,讓它在遍歷 AST 時被各元素「接受」。例如:

  • 型別檢查:建立 TypeCheckingVisitor,各節點的 Accept 操作呼叫 visitor 上對應的方法(如 VisitAssignment、VisitVariableReference)
  • 程式碼生成:建立 CodeGeneratingVisitor,無需修改節點類別

這形成兩個類別階層:Element 階層(被操作的物件)和 Visitor 階層(定義操作)。新增操作只需新增 Visitor 子類別,不需修改 Element。

適用場景(Applicability)#

  • 物件結構包含許多不同介面的類別,你需要根據具體類別執行不同操作
  • 需要對物件結構執行多種不相關的操作,又不想用這些操作「污染」元素類別。Visitor 將相關操作集中定義在一個類別中
  • 物件結構的類別很少變動,但經常需要定義新操作。若元素類別經常變動,每次都需修改所有 Visitor 的介面,代價太高

Visitor 模式的適用前提是元素類別階層穩定。若經常新增 ConcreteElement,每個新元素都需要在 Visitor 介面新增抽象方法並在所有 ConcreteVisitor 中實作,維護成本極高。此時不如直接在元素類別中定義操作。

結構(Structure)#

  • Visitor 為物件結構中每種 ConcreteElement 宣告一個 Visit 操作
  • ConcreteVisitor 實作 Visitor 宣告的各個操作,提供演算法的上下文和累積狀態
  • Element 定義接受 Visitor 的 Accept 操作
  • ConcreteElement 實作 Accept,呼叫 Visitor 上對應自身類別的操作
  • ObjectStructure 可列舉其元素,提供高層介面讓 Visitor 拜訪各元素
classDiagram
    class Visitor {
        <<interface>>
        +VisitConcreteElementA(ConcreteElementA)
        +VisitConcreteElementB(ConcreteElementB)
    }
    class ConcreteVisitor1 {
        +VisitConcreteElementA()
        +VisitConcreteElementB()
    }
    class ConcreteVisitor2 {
        +VisitConcreteElementA()
        +VisitConcreteElementB()
    }
    class Element {
        <<interface>>
        +Accept(Visitor)
    }
    class ConcreteElementA {
        +Accept(Visitor)
        +OperationA()
    }
    class ConcreteElementB {
        +Accept(Visitor)
        +OperationB()
    }
    class ObjectStructure
    Visitor <|.. ConcreteVisitor1
    Visitor <|.. ConcreteVisitor2
    Element <|.. ConcreteElementA
    Element <|.. ConcreteElementB
    ObjectStructure o--> Element
    ConcreteElementA ..> Visitor : accept
    ConcreteElementB ..> Visitor : accept

參與者(Participants)#

參與者範例職責
VisitorNodeVisitor為每個 ConcreteElement 類別宣告一個 Visit 操作
ConcreteVisitorTypeCheckingVisitor實作各個 Visit 操作,提供演算法片段並維護遍歷過程中的累積狀態
ElementNode定義 Accept 操作,接受 Visitor 作為引數
ConcreteElementAssignmentNode、VariableRefNode實作 Accept,回呼 Visitor 上對應的操作
ObjectStructureProgram列舉元素,可能是 Composite 或集合

協作方式(Collaborations)#

  • 客戶端建立 ConcreteVisitor,然後遍歷物件結構,讓每個元素以 visitor 為引數呼叫 Accept
  • 元素被拜訪時,呼叫 Visitor 上對應自身類別的操作,並將自身作為引數傳入,讓 Visitor 存取其狀態
sequenceDiagram
    participant Client
    participant ObjectStructure
    participant ConcreteElementA
    participant ConcreteElementB
    participant ConcreteVisitor
    Client->>ObjectStructure: Accept(visitor)
    ObjectStructure->>ConcreteElementA: Accept(visitor)
    ConcreteElementA->>ConcreteVisitor: VisitConcreteElementA(this)
    ObjectStructure->>ConcreteElementB: Accept(visitor)
    ConcreteElementB->>ConcreteVisitor: VisitConcreteElementB(this)

優缺點(Consequences)#

優點:

  • 容易新增操作:新增一個 Visitor 子類別即可定義新操作,不需修改元素類別。相比之下,將操作分散在各元素類別中,每次新增操作都需修改所有類別
  • 集中相關操作、分離無關操作:相關行為集中在同一個 Visitor 中,而非散布在各元素類別。不相關的行為各自在不同的 Visitor 子類別中,演算法的資料結構也可隱藏在 Visitor 內部
  • 跨類別階層拜訪:Iterator 只能拜訪具有共同父類別的物件;Visitor 沒有這個限制,可以拜訪不具繼承關係的物件
  • 累積狀態:Visitor 在遍歷過程中可以累積狀態,不需要額外的引數或全域變數

缺點:

  • 新增 ConcreteElement 困難:每新增一個 ConcreteElement,就需要在 Visitor 介面新增抽象操作,並在每個 ConcreteVisitor 中實作
  • 破壞封裝:Visitor 需要 ConcreteElement 提供足夠的公開介面來完成工作,這可能暴露元素的內部狀態

選擇 Visitor 模式的關鍵判斷:你更常改變的是操作還是元素結構?若元素結構穩定但經常新增操作,Visitor 是好選擇;若元素經常變動,直接在元素類別中定義操作更合適。

實作要點(Implementation)#

  • Double Dispatch:Visitor 模式的關鍵技術。在 single-dispatch 語言(如 C++、Java)中,呼叫哪個方法取決於接收者的型別。Double dispatch 讓執行的操作同時取決於 Visitor 的型別和 Element 的型別。Accept 就是 double dispatch 操作——其語義取決於 Visitor 和 Element 兩者的型別
  • 誰負責遍歷物件結構
    • 物件結構自身:集合類別迭代元素並對每個元素呼叫 Accept,Composite 則遞迴遍歷子元素
    • Visitor 自身:將遍歷邏輯放在 Visitor 中,適合遍歷順序依賴於操作結果的複雜情況,但會在每個 ConcreteVisitor 中重複遍歷程式碼
    • 獨立的 Iterator:外部或內部迭代器皆可

將遍歷責任放在 Visitor 中的主要理由是實現特別複雜的遍歷——例如遍歷順序取決於前面元素的操作結果。書中以正規表示式匹配為例:RepeatExpression 需要重複遍歷其子元素,這種不規則的遍歷邏輯適合由 Visitor 掌控。

已知應用(Known Uses)#

  • Smalltalk-80 編譯器:ProgramNodeEnumerator 是一個 Visitor 類別,主要用於原始碼分析的演算法
  • IRIS Inventor(3D 圖形工具包):場景以節點階層表示,不同操作(渲染、事件處理、搜尋、計算邊界框)由不同的 Visitor(稱為「actions」)實現。使用 double dispatch 機制與二維表格將 Visitor 和節點類別映射到對應函式
  • X Consortium 的 Fresco:Mark Linton 在此規範中創造了「Visitor」這個術語

相關模式(Related Patterns)#

  • Composite:Visitor 常用於對 Composite 定義的物件結構執行操作
  • Interpreter:Visitor 可以用於對 Interpreter 模式中的語法樹執行解釋操作