本章主軸#

抽象工廠(Abstract Factory)用來協調一群相關物件的實例化。當系統需要根據情境一次拿到一整組搭配正確的物件時,就是它的舞台。本章用「不同效能機器要用不同解析度的驅動程式」為例,逐步推導出 Abstract Factory。

GoF 對 Abstract Factory 的意圖描述#

提供一個介面,用來建立一群相關或相依的物件,而不需指定它們的具象類別。

推導範例:依機器選擇驅動程式#

場景#

驅動類別低效能機器高效能機器
Display 驅動LRDD(低解析顯示)HRDD(高解析顯示)
Print 驅動LRPD(低解析列印)HRPD(高解析列印)

「家族(family)」按應用場景區分——例如低解析家族、高解析家族;中階機器也可能混搭。

嘗試一:直接用 switch#

ApControl 內以 switch 決定要 new 哪種驅動。問題:

  • 緊密耦合:若新增「中階」這個解析度層級,要改多個 switch
  • 弱凝聚doDrawdoPrint 同時負責畫圖/列印與選驅動

Switch 通常暗示兩件事:

  1. 應該改用多型
  2. 責任放錯地方

嘗試二:以繼承特化 ApControl#

Figure 11-1: 以繼承處理變化——LowResApControl 與 HighResApControl 各自實例化對應驅動

每組(低效能、高效能、…)各做一個 ApControl 子類。問題依舊:

  • 組合爆炸:每多一個變動軸就乘一倍
  • 類別意義模糊
  • 違反「偏好聚合勝於繼承」

嘗試三:抽象化 + 工廠物件#

  • 把驅動抽象成 DisplayDriverPrintDriverApControl 只面向抽象操作

Figure 11-2: 將驅動程式抽象化——DisplayDriver 與 PrintDriver

  • 引入 ResFactory 來「提供當前該用的驅動」

Figure 11-3: 理想情況——ApControl 只面對抽象 DisplayDriver 與 PrintDriver

Figure 11-5: ResFactory 抽象類別與 LowResFact/HighResFact 兩個具象工廠

  • ApControlResFactory 各司其職:使用 vs 建立
  • LRDD/HRDD 若不來自同一族系,就用 Adapter 包成相同抽象

Figure 11-6: 中間方案——ApControl 透過 ResFactory 取得驅動

Figure 11-7: 結合 Abstract Factory 與 Adapter——讓不同族系的驅動共享抽象

abstract class ResFactory {
    abstract public DisplayDriver getDispDrvr();
    abstract public PrintDriver getPrtDrvr();
}

class LowResFact extends ResFactory {
    public DisplayDriver getDispDrvr() { return new LRDD(); }
    public PrintDriver  getPrtDrvr()  { return new LRPD(); }
}

class HighResFact extends ResFactory {
    public DisplayDriver getDispDrvr() { return new HRDD(); }
    public PrintDriver  getPrtDrvr()  { return new HRPD(); }
}

ResFactory 抽象化、由不同的具象工廠負責不同家族——這就是 Abstract Factory。「Abstract」指的是被建立出來的東西本身是抽象,而非工廠類別自己。

為什麼叫 Abstract Factory?#

不是因為工廠類別本身是抽象的,而是因為所建立出來的物件是由抽象來指定——DisplayDriverPrintDriver 都是抽象。怎麼實作工廠變化是另一回事。

三個關鍵問題的歸屬#

問題原本解法Abstract Factory 解法
我目前是哪個 case?ApControl 自己知配置檔指定要建立哪個具象工廠
如何管理這些資訊?ApControl 管每個具象工廠自己知道要做什麼
建構邏輯放哪裡?ApControl 裡工廠物件裡

「不就又用回 switch 了嗎?」是的,但 switch 被孤立到工廠裡,不再與使用方耦合,影響面非常小。

Abstract Factory 的關鍵特性#

欄位內容
Intent為特定 client 或 case 提供整組物件
Problem一群相關物件需被協調地實例化
Solution把實例化規則從使用方抽出,集中到工廠物件
ParticipantsAbstractFactory 定義「該建立哪些家族成員」;ConcreteFactory 各自實作
Consequences把「哪個物件要用」與「如何用該物件」隔開
Implementation抽象工廠類別 + 每個家族一個具象工廠(也可改用配置檔、資料表)

Figure 11-8: Abstract Factory 模式的通用結構——AbstractFactory 建立 AbstractProductA/B 家族

實務筆記#

何時適用#

需要根據以下任一變動軸提供不同物件家族時:

  • 不同作業系統(跨平台應用)
  • 不同效能等級
  • 不同版本
  • 不同使用者特性
  • 不同地區設定(locale-dependent 對話框、日期格式)

變體:用配置檔代替具象工廠#

家族數量太多時,不必每個家族都做一個具象工廠:

  • 配置檔指定要 new 的類別名稱
  • Java/C# 透過 Class.forName 等機制動態實例化
  • 資料表的每一列代表一個家族

Adapter 經常一起出現#

不同家族的成員不一定來自同一基底類別。LRDD、HRDD 常常各自獨立,這時就需要先用 Adapter 把它們適配成共同的抽象介面,Abstract Factory 才能順利運作。

取得正確工廠的方式#

通常透過配置檔讀取:依配置內容實例化對應的具象工廠。或由主系統決定後,再把工廠物件傳入子系統。

與 CAD/CAM 問題的關聯#

CAD/CAM 系統需要「全套 V1 features」或「全套 V2 features」。Abstract Factory 正好用來保證實際拿到的這群 feature 物件版本一致。

本章要記住的事#

  • Abstract Factory 的關鍵:所建立的物件以抽象指定;族與族之間搭配一致
  • 把「使用」與「建立」徹底解耦
  • switch 不是壞事,位置擺對才不會傳染整個系統
  • 配置檔 / 反射可以讓 Abstract Factory 動態化,避免具象工廠數量爆炸
  • 它常與 Adapter 並肩工作