意圖(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)#
| 參與者 | 範例 | 職責 |
|---|---|---|
| Visitor | NodeVisitor | 為每個 ConcreteElement 類別宣告一個 Visit 操作 |
| ConcreteVisitor | TypeCheckingVisitor | 實作各個 Visit 操作,提供演算法片段並維護遍歷過程中的累積狀態 |
| Element | Node | 定義 Accept 操作,接受 Visitor 作為引數 |
| ConcreteElement | AssignmentNode、VariableRefNode | 實作 Accept,回呼 Visitor 上對應的操作 |
| ObjectStructure | Program | 列舉元素,可能是 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 模式中的語法樹執行解釋操作