軟體設計

Swift 橋接模式(Bridge Pattern):分離抽象化,避免子類別爆炸

使用 Swift 開發 App 時,總會遇到類別不斷增加的時刻。

閱讀 4 分鐘
Swift 橋接模式(Bridge Pattern):分離抽象化,避免子類別爆炸 封面圖

使用 Swift 開發 App 時,總會遇到類別不斷增加的時刻。

明明只新增一個按鈕,卻要分成淺色模式、深色模式、iOS、iPadOS 版本;組合越多,類別數量就越容易爆炸。

先說結論。Swift 橋接模式會把「做什麼(抽象化)」與「如何做(實作)」分成不同的類別階層,讓兩個軸以相加而非相乘的方式管理

本文會依序說明子類別爆炸的原因、橋接模式如何避免,以及實際的 Swift 程式碼。


為什麼會發生子類別爆炸?

當兩個彼此不同的變化軸只能用繼承表達時,就會發生子類別爆炸。

來看一個例子。

假設有一個 Remote 類別,可以用它操作 TV 和收音機。

如果還需要「基本遙控器」和「進階遙控器」兩種類型呢?

組合會變成這樣。

  • 基本遙控器 × TV
  • 基本遙控器 × 收音機
  • 進階遙控器 × TV
  • 進階遙控器 × 收音機

這樣就已經有 4 個了。

裝置加入「喇叭」後變成 6 個,遙控器加入「語音遙控器」後則增加到 9 個。

有兩個軸時,只使用繼承會讓類別數量以乘法(m×n)增加,而不是加法(m+n)。

這就是子類別爆炸。軸越多,維護負擔就會呈幾何級數增加。


Swift 橋接模式如何避免這個問題?

核心只有一個。

把兩個變化軸各自拆成獨立的階層,再透過參考(組合)連接兩者。

  • 抽象化層:遙控器類型(基本/進階)
  • 實作層:裝置類型(TV/收音機)

遙控器不會繼承裝置,而是將裝置介面以屬性形式持有。

先將實作層定義成協定,只放入裝置必須提供的最小操作。

// 實作層:裝置必須提供的最小功能
protocol Device {
    var volume: Int { get set }
    func enable()
    func disable()
}

現在,作為抽象化層的遙控器只需要參考 Device。這不是繼承,而是注入。

// 抽象化層:持有裝置並委派工作
class RemoteControl {
    let device: Device          // 橋接連接點
    init(device: Device) { self.device = device }
    func togglePower() { /* device.enable/disable 呼叫 */ }
}

這個 device 屬性就是連接兩個階層的橋(Bridge)。

遙控器不繼承裝置,只持有它
遙控器不繼承裝置,只持有它

實際擴充後會有什麼不同?

這是我親自擴充程式碼後感受到的差異。

新增進階遙控器時,只要繼承 RemoteControl,在遙控器階層新增一個類別即可。

// 擴充抽象化:裝置程式碼完全不用修改
class AdvancedRemote: RemoteControl {
    func mute() {
        var d = device
        d.volume = 0        // 委派給實作層
    }
}

反過來,新增裝置(例如喇叭)時,只要採用 Device 協定即可。遙控器程式碼仍可直接重複使用。

兩個軸完全獨立,因此擴充其中一邊時,不必重新建立另一邊的組合。

用表格比較,差異就很明顯。

分類 只使用繼承 橋接模式
類別增加方式 乘法(m×n) 加法(m+n)
3 種遙控器 × 4 種裝置 12 個 7 個
新增裝置時 增加與遙控器種類相同的數量 只增加 1 個
執行階段替換 困難 可透過注入自由替換

以 3 種遙控器、4 種裝置為例,類別會從 12 個降為 7 個。軸越多,差距越明顯。

原本以乘法增加的類別,透過一座橋就能改為加法
原本以乘法增加的類別,透過一座橋就能改為加法

什麼時候該用,什麼時候該避免?

橋接模式不是所有情況的答案。到處濫用,反而容易讓程式碼變複雜。

適合使用的情況:

  1. 存在兩個以上彼此獨立變化的軸
  2. 想在執行階段替換實作時(例如實際 API ↔ Mock)
  3. 想清楚分離不同平台與主題的實作時

可以不使用的情況:

  • 只有一個軸,或組合未來也會固定在 2~3 個時
  • 結構很簡單,卻先拆成階層而造成過度設計時

Swift 原本就很自然地使用協定與相依性注入,因此有時只是叫不出橋接模式的名字,實際上早已在使用它。


常見問題(Q&A)

Q. 這和 Adapter Pattern 有什麼不同?

Adapter 的目的是事後接上已完成但彼此不相容的程式碼;Bridge 的目的是在設計階段就先分離兩個軸。

Q. 一定要使用 protocol 嗎?

是。在 Swift 中,將實作層定義成 protocol 最自然。使用協定取代抽象類別後,值型別(struct)也能作為實作。

Q. SwiftUI 也能使用嗎?

可以。讓 View 注入資料來源協定,只負責渲染,這本身就是橋接結構。

如果看出有兩個軸,像這樣拆開會方便很多
如果看出有兩個軸,像這樣拆開會方便很多

如果你一直因為子類別不斷增加而感到困擾,今天只要記住一件事。

當你覺得「這個類別有兩個變化原因」時,就是拆分兩個軸的時機。

先親手改寫一個小例子,很快就會熟悉。希望你的程式碼能變得更輕盈!

延伸閱讀