進行 iOS 開發時,總會遇到需要持續對圖形或節點等多種類型物件加入運算的情況。
你通常會用 if let、as?逐一檢查型別;每增加一種型別,就得再次修改分支。
本文透過範例整理 Swift Visitor 模式,以及其核心雙重分派究竟何時有必要。
Swift 選擇方法時,只會查看接收器(接收訊息的物件)的動態型別。
當「是哪種圖形」和「是哪種運算」兩個維度同時變動時,單一分派就不夠了。雙重分派會透過兩次呼叫,在執行階段決定兩個型別。
Swift 的方法分派,為什麼只靠一次還不夠?
在 Swift 中呼叫 shape.draw() 時,會執行符合 shape實際型別的 draw()。
這就是單一分派:只根據接收器型別 ****選擇方法。
問題會在運算有多個時出現。
圖形包含圓形、矩形、三角形;運算包含繪製、計算面積、輸出 JSON。兩個維度相乘後,組合數會快速增加。
因此常會寫出這樣的程式碼。
// 每增加一種圖形運算,程式就會持續變長 if-else
func export(_ shape: Shape) -> String {
if let c = shape as? Circle { return drawCircle(c) }
if let r = shape as? Rect { return drawRect(r) }
return "" // 每次新增圖形,都得再次修改這裡
}
如果新增一個圖形,就得開啟這個函式,再插入一個分支。
有五個運算,就等於有五個這樣的函式。
什麼時候需要雙重分派?
我的判準很簡單。
當方法要執行的行為同時依賴兩個型別時,就是需要雙重分派的時機。
前面的例子正是如此:要執行的程式碼同時取決於「圖形型別 × 運算型別」。
Swift 的預設分派只查看接收器型別,因此另一個維度只能用 as?這類型別檢查手動處理。
這種手動分支就是 code smell。
雙重分派連第二個型別的決定也交給語言的多載,開發者不必再自行檢查型別。
反過來說,如果只有一個運算,或型別不會再增加,就不必使用這個模式。
Swift Visitor 模式要如何實作?
關鍵是把呼叫分成兩次,所以才稱為雙重分派。
protocol Shape { func accept(_ v: Visitor) -> String }
struct Circle: Shape { func accept(_ v: Visitor) -> String { v.visit(self) } }
struct Rect: Shape { func accept(_ v: Visitor) -> String { v.visit(self) } }
protocol Visitor {
func visit(_ c: Circle) -> String // 圖形型別在這裡決定
func visit(_ r: Rect) -> String // 運算則由多載決定
}
一起看看流程。
第一次呼叫 shape.accept(v)會決定 圖形的實際型別。執行階段會選擇呼叫 Circle的 accept,還是 Rect的版本。
第二次呼叫 v.visit(self)時,self的靜態型別已確定為 Circle或Rect。因此能正確選出多載的 visit。
兩次呼叫彼此銜接,便能精確決定兩個型別。
想新增運算時,只要建立一個採用 Visitor 的新結構即可。既有圖形程式碼完全不用修改。
Visitor 模式該不該用?優缺點比較
沒有完美的模式,接著看看取捨。
| 分類 | 型別分支(as? / switch) | Visitor 模式 |
|---|---|---|
| 新增運算 | 修改各處的分支 | 新增一個 Visitor |
| 新增型別 | 新增一行分支 | 需要修改所有 Visitor |
| 程式碼可讀性 | 分支增加後變複雜 | 依運算清楚分離 |
| 初始撰寫成本 | 低 | 相對較高 |
如表所示,Visitor 模式在運算經常增加、型別保持穩定時最能發揮效果。
相反地,如果型別(圖形)持續增加,反而更費工,因為所有 Visitor 都要修改。
常見問題(Q&A)
Q. Swift 已經有泛型,還需要 Visitor 嗎?
泛型在編譯時確定型別時很強大。但當實際型別在執行階段混雜於[Shape]陣列中時,雙重分派仍能發揮作用。
Q. 用 enum 和 switch 不也可以嗎?
可以。如果型別固定,enum + switch 反而更簡潔。Visitor 更適合運算持續增加的情況。
遇到同時依賴兩個型別的行為時,就是該想起雙重分派與 Swift Visitor 模式的時候。
如果現在有 as?分支不斷增加的程式碼,請參考今天的範例試著重構,結構會更容易維護 🙂

