軟體設計

Swift 抽象工廠模式:一次替換整組相關物件

抽象工廠會將多個彼此相容的物件組成產品族群,並一次完成替換。本文以 Swift 主題為例,說明如何防止組合錯誤,以及如何選擇抽象工廠或工廠方法。

閱讀 4 分鐘
Swift 抽象工廠模式:一次替換整組相關物件 封面圖

你是否曾經把 App 畫面主題從淺色整套切換成深色?

如果逐一替換按鈕和標籤的顏色,總會漏掉某個地方。很容易出現按鈕是黑色背景,標籤卻還是白色背景的問題。

這時 Swift 的抽象工廠模式就很適合。把相關物件綁成一組,再整組替換。

修改單一物件建立位置的模式,和建立整個產品族群的模式,適用範圍不同。想先釐清界線,請一併參考 工廠方法 vs 抽象工廠的差異

本文將整理抽象工廠模式究竟解決什麼問題、如何用 Swift 程式碼一次替換相關物件,以及何時適合或不適合使用。


什麼是抽象工廠模式?

用一句話回答就是:

依情況整個替換一座工廠,而這座工廠會將必須相容的物件成組建立。

核心是「必須一起使用的物件」。深色主題的按鈕和淺色主題的背景混在一起,就會造成問題。

抽象工廠能避免這類組合錯誤。

選定一座工廠後,它產出的零件就會自動互相搭配。選擇淺色工廠,就會得到淺色按鈕、淺色標籤和淺色背景。

不是逐一選擇零件,而是直接選擇整座工廠。

這就是抽象工廠的全部概念。


在 Swift 中一次替換相關物件

放到實際程式碼中會清楚許多。以下以 UI 主題為例。

先定義要建立之零件的規格(協定),以及產生這些零件的工廠規格。

protocol Button { func render() -> String }
protocol Label  { func render() -> String }

// 工廠負責整組彼此搭配的零件
protocol ThemeFactory {
    func makeButton() -> Button
    func makeLabel() -> Label
}
一切都從定義一個 protocol 開始
一切都從定義一個 protocol 開始

接著分別建立淺色主題工廠和深色主題工廠。每座工廠只產生符合自身主題的零件。

選定一座工廠,零件就會自動成套帶來
選定一座工廠,零件就會自動成套帶來
struct LightFactory: ThemeFactory {
    func makeButton() -> Button { LightButton() }
    func makeLabel() -> Label  { LightLabel() }
}
struct DarkFactory: ThemeFactory {
    func makeButton() -> Button { DarkButton() }
    func makeLabel() -> Label  { DarkLabel() }
}

繪製畫面的程式只要取得一座工廠即可,不必在意它是哪一座工廠。

func buildScreen(with factory: ThemeFactory) {
    let button = factory.makeButton()
    let label  = factory.makeLabel()
    print(button.render(), label.render())
}

// 替換主題只要修改工廠的一行程式碼
buildScreen(with: DarkFactory())

LightFactory()替換成 DarkFactory()的瞬間,按鈕和標籤就會一起切換成深色。

不必再分別修改按鈕和標籤的顏色了。

從淺色到深色,只改一行畫面就完美搭配
從淺色到深色,只改一行畫面就完美搭配

它和工廠方法有什麼不同?

名稱相似,很容易混淆,這裡先說明兩者的差異。

分類 工廠方法 抽象工廠
建立對象 一種物件 多種相關物件(整組)
重點 「要建立什麼?」 「將相容的物件綁在一起」
代表範例 建立一個按鈕 按鈕+標籤+背景一整套

簡單來說,工廠方法是建立單一零件的方法,而抽象工廠是把這些零件組成一整套的方法。

也可以把它理解成:抽象工廠中使用了多個工廠方法。


何時適合使用,何時應避免?

實際使用後,我發現適合的情境和反而過度設計的情境區分得很明顯。

適合的情況

  • 當主題、平台、國家等明確以「整組」為單位變更時
  • 當需要像 iOS 和 macOS 一樣整組替換 UI 元件時
  • 當付款方式、資料庫驅動程式等需要成套替換時

這些情況會過度設計

  • 只需要建立一兩種物件時
  • 組合規則經常變動,導致整組本身不穩定時

明明只有一個物件,卻建立抽象工廠,反而會增加程式碼並降低可讀性。

在有明確理由要組成一整套時再使用它比較好。


兩個常見問題

Q. 之後想新增零件種類怎麼辦?

需要在協定中新增方法,並在所有工廠實作該方法。工廠很多時會相當費工,因此若專案中的零件種類會經常增加,最好事先考慮。

Q. SwiftUI 也會使用嗎?

當然可以,但 SwiftUI 已經提供 Environment@Observable 這類內建注入方式,因此只處理主題時,通常那樣更簡潔。若是 UIKit 基礎架構,或純邏輯層需要替換整組物件,抽象工廠會更有價值。


抽象工廠終究是一種「用同一個把手綁住不能各自運作的事物」的模式。

試過只改一行主題就讓整個畫面完美搭配後,你就會立刻理解為什麼要使用這個模式。建議先從小型範例親手實作。

延伸閱讀