Overview of Content
Visitor 是將資料結構 & 資料處理分離,是設計模式中最複雜的一個
Visitor 設計模式是一種行為型設計模式,它主要用於將操作(算法)與元素的結構分離開來,使得在不改變元素結構的前提下,可以定義新的操作。這種模式通常用於訪問一組相關的元素,並在不同的元素上執行不同的操作,而不影響元素結構。
本篇文章將深入探討 Visitor 設計模式的基本概念、使用場景、定義及 UML 圖,以及其優缺點。我們將透過實際案例展示 Visitor 模式的實現方式,同時介紹如何擴展 Visitor 模式以應對更複雜的需求。此外,我們還將討論 Single dispatch
和 Double dispatch
的概念,深入解釋這兩種分派方式在 Visitor 設計模式中的應用。
如果你對於如何有效地處理元素結構中的不同操作感到困惑,或者希望實現一個靈活且可擴展的操作系統,那麼本文將為你提供深入的解說和實用的示例。
寫文章分享不易,如有引用參考請詳註出處,如有指導、意見歡迎留言(如果覺得寫得好也請給我一些支持),感謝 😀
Visitor 使用場景
● 物件結構穩定(若結構不穩定,這個設計模式的結構就會常常被更動,甚至影響到已經實作的子類),但需要常在該物件上定義新的操作
● 假設一物件由多個物件聚合而成,這些物件都有一個類用來接收訪問者的存取,而訪問者是一個界面,對於實做這個界面,存取到物件結構中不同類型的元素做出不同的處理
解開接收者、訪問者的耦合性
● 對一物件結構中物件進行 不同
並 不相關
的操作,避免該物件被汙染,也不希望新增操作時修改到該類別
如果需求是:需要不改變封裝,並對於數據有不同操作模式後,就可以使用
Visitor
模式
這個模式也非常適合用於 大規模的重構項目(因為需求已經很清晰、變動性不大),但 如果項目的需求不夠穩定、清晰,那建議先不要使用這個模式
Visitor 定義 & Visitor UML
● 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);
}
... 省略部份
}
更多的物件導向設計
物件導向的設計基礎如下,如果是初學者或是不熟悉的各位,建議可以從這些基礎開始認識,打好基底才能走個更穩(在學習的時候也需要不斷回頭看)!
創建模式 Creation Patterns
● 創建模式 PK
● 創建模式 - Creation Patterns
:
創建模式用於「物件的創建」,它關注於如何更靈活、更有效地創建物件。這些模式可以隱藏創建物件的細節,並提供創建物件的機制,例如單例模式、工廠模式… 等等,詳細解說請點擊以下連結
● Singleton 單例模式 | 解說實現 | Android Framework Context Service
● Abstract Factory 設計模式 | 實現解說 | Android MediaPlayer
● Factory 工廠方法模式 | 解說實現 | Java 集合設計
● Builder 建構者模式 | 實現與解說 | Android Framwrok Dialog 視窗
● Clone 原型模式 | 解說實現 | Android Framework Intent
行為模式 Behavioral Patterns
● 行為模式 PK
● 行為模式 - Behavioral Patterns
:
行為模式關注物件之間的「通信」和「職責分配」。它們描述了一系列物件如何協作,以完成特定任務。這些模式專注於改進物件之間的通信,從而提高系統的靈活性。例如,策略模式、觀察者模式… 等等,詳細解說請點擊以下連結
● Stragety 策略模式 | 解說實現 | Android Framework 動畫
● Interpreter 解譯器模式 | 解說實現 | Android Framework PackageManagerService
● Chain 責任鏈模式 | 解說實現 | Android Framework View 事件傳遞
● Specification 規格模式 | 解說實現 | Query 語句實做
● Command 命令、Servant 雇工模式 | 實現與解說 | 物件導向設計
● Memo 備忘錄模式 | 實現與解說 | Android Framwrok Activity 保存
● Visitor 設計模式 | 實現與解說 | 物件導向設計
● Template 設計模式 | 實現與解說 | 物件導向設計
● Mediator 模式設計 | 實現與解說 | 物件導向設計
● Composite 組合模式 | 實現與解說 | 物件導向設計
● Observer 觀察者模式 | JDK Observer | Android Framework Listview
結構模式 Structural Patterns
● 結構模式 PK
● 結構模式 - Structural Patterns
:
結構模式專注於「物件之間的組成」,以形成更大的結構。這些模式可以幫助你確保當系統進行擴展或修改時,不會破壞其整體結構。例如,外觀模式、代理模式… 等等,詳細解說請點擊以下連結
● Decorate 裝飾模式 | 解說實現 | 物件導向設計
● Iterator 迭代設計 | 解說實現 | 物件導向設計