IThome 鐵人賽
設計模式
Abstract Factory
Creational Patterns
Codetopia
Codetopia 的市長辦公室(就是那個獨一無二的 Singleton!)頒布了一項新命令:「城市視覺風格週」正式啟動!要求全市所有公共設施,必須在一夜之間,換上風格完全一致的「科技未來風」主題。
這下可忙壞了採購科的專員 Mia。她的辦公桌被四家不同供應商的業務代表團團圍住。
「Mia 小姐,我們家的路牌,絕對是賽博龐克首選!」
「我們家的號碼牌,極簡線條,科技感十足!」
「這是我們的電子表單框樣式,參考一下?」
「還有我們的制服!絕對跟得上潮流!」
Mia 一個頭兩個大。她發現,每一項單品看起來都很棒,但當她試圖將 A 家的路牌、B 家的表單和 C 家的印章湊在一起時… 畫面簡直慘不忍睹。供應商們只保證自家產品的品質,卻沒有一個機制能保證他們彼此之間能「搭得起來」。
危機警報在 Mia 腦中大響 🚨。如果她像昨天一樣,用 Factory Method 來逐項分流櫃台,那明早的頭條新聞絕對是:「Codetopia 市容災難:未來風路牌配上復古木質風印章,市民美感集體崩潰!」
這一次,問題的核心不再是「如何生產單一物件」,而是「如何確保一整組物件的風格一致性」。這就是產品家族的挑戰與價值所在。
在 Mia 找到解決方案前,整個採購流程簡直就是一場「拼裝車大賽」,充滿了各種令人噴飯的壞味道:
「拼裝城市」風 🚗:路牌是 A 廠的,號碼牌是 B 廠的,表單是 C 廠的。每個看起來都不錯,組合起來卻像一輛用膠帶黏起來的報廢車,跑起來隨時會散架。
「風格判斷 if-else」散落全城 🤧:更可怕的是,為了「看起來」一致,程式碼裡到處都是 if (style == "techno") { set_techno_border(); } else if (style == "wood") { set_wood_texture(); }
。從申請表單的邊框,到承辦員制服的鈕扣,每個小元件都在獨立進行一次風格判斷。這根本不是城市規劃,這是全民大家來找碴!
「下週就回不去」的單行道 😭:市長突然又說:「下週我們改走溫暖木質風!」Mia 眼前一黑。這意味著她和工程師團隊得把整個城市所有程式碼翻過來,把上百個 techno
字串手動換成 wood
。萬一漏掉一個,災難重演。
[拍案] 說白了,這一切混亂的根源就在於:所謂的「風格一致性」,全靠開發人員用肉眼來驗收,系統本身沒有任何機制來保證一個「家族」的完整性。
為了解決這場「混搭災難」,Mia 搬出了今天的王牌工法——抽象工廠模式 (Abstract Factory)。
這同樣是一種創生型模式,但它的野心比 Factory Method 大得多。它的核心思想是:
提供一個介面,用於建立一系列相關或相互依賴的物件(一個產品家族),而無需指定它們具體的類別。
用 Codetopia 的話來說,就是成立一個「供應商聯盟 (ThemeFactory)」!
我們不直接跟單一的「路牌廠」或「制服廠」打交道。
我們只跟「科技未來風聯盟」或「溫暖木質風聯盟」簽約。
一旦選定了聯盟(具體的工廠),這個聯盟就必須負責提供一整套風格相容的路牌、印章、表單...等等。
這樣一來,我們就把「確保一致性」的責任,從 Mia 和開發者身上,轉移到了聯盟這個抽象層級。
✅ 當你需要「以家族為單位」工作時:你的系統需要一整組產品,而且這些產品必須來自同一個「家族」(例如:同一種視覺主題、同一個作業系統的 UI 元件)。
✅ 當你希望系統獨立於其產品的建立、組合和表示時:客戶端程式碼只依賴抽象介面,完全不知道背後是哪個聯盟在生產。
✅ 當你想在工廠層級強制執行一致性約束時:一致性由聯盟來保證,而不是靠客戶端的人工檢查或 if-else
。
⛔ 你只需要一個產品,且它與其他產品沒有關聯:殺雞焉用牛刀。如果只是要生產昨天的 Clerk
,Factory Method 就綽綽有餘了。
⛔ 產品家族內的物件之間,沒有太強的一致性約束:如果路牌和印章的風格本來就不需要搭配,那硬是把它們綁在同一個聯盟裡,反而會降低彈性。
⛔ 產品家族的介面不穩定,經常需要增加新產品:每次在家族中增加一個新產品(例如從今天起,所有風格都要額外提供「印台」),你就必須修改抽象工廠的介面,這會導致所有實作該介面的具體聯盟都需要跟著修改,違反開閉原則。
[旁批] 過渡解:減痛替代方案
如果你的產品家族還在快速演化,邊界未定,直接上 Abstract Factory 可能會讓你因為頻繁修改介面而痛苦不堪。這時可以考慮:
先用 Factory Method + 組態檔:用多個工廠方法分別生產單品,靠外部組態檔來約束它們的組合關係。
搭配橋接模式 (Bridge Pattern):將「風格」這個維度與「功能」維度分離,讓它們可以獨立變化,待風格穩定後再考慮用 Abstract Factory 固化下來。
導播,鏡頭拉一下!讓我們用三種不同的焦段,來看看這個供應商聯盟是怎麼運作的。
[旁批] 這區塊技術含量比較高,就像是總設計師的施工小抄。看不懂?沒關係,記得結論就好:「以前東拼西湊,聯盟一下,全都對齊!」
視角 | 觀念/模式 | 在 Codetopia 的說法 |
---|---|---|
微觀 (GoF) | Abstract Factory | 供應商聯盟 (ThemeFactory) 產出成套的 Form, Stamp, Signage 家族 |
中觀 (訊息/事件) | 事件觸發,動態更換 Provider | 「主題切換 (ThemeChanged)」事件,觸發「樣式服務」更換聯盟實例 |
宏觀 (MAS) | 代理透過黃頁 (DF) 查詢能力 | 採購單位向「黃頁」查詢:哪個聯盟具備「科技未來風」的生產能力? |
契約測試:每個聯盟都必須遵守 IThemeFactory
的契約,同時實作 IForm
, IStamp
, ISignage
三件產品,否則在整合測試階段就會立刻被標記為違約,直接 Fail!這就把「一致性」從人眼判斷,升級為機器自動驗證。
這張圖展示了聯盟的契約(抽象工廠),以及兩個實際的供應商(具體聯盟)如何履行合約。
這張圖展示了 Mia 如何透過一個統一的「城市樣式服務」,僅用一個指令就完成全城的風格切換。
[旁批] 注意看流程!所有請求都由樣式服務統一調度。
讀圖口訣:「應用端拿成品,不拿工廠。」
這就確保了風格決策權集中,避免了判斷邏輯散落全城的災難。
在動手實作前,先確立我們的故障處理策略。當一個未知的風格被請求時,有兩種主流做法:回退(Fallback) 或 拒絕(Fail Fast)。前者會提供一個預設風格並記錄告警,確保系統可用性;後者則會直接拋出錯誤(如 HTTP 503),避免系統呈現出風格不一致的「半殘」狀態。本次實作我們選擇後者,以求明確。
[旁批] 客戶端只見抽象,不見具體。這就是解耦的魔力!
讓我們用 Python 來實現這個優雅、可擴充的供應商聯盟機制。
from abc import ABC, abstractmethod
from typing import Dict, Type
# --- 1. 定義產品家族的抽象介面 ---
class IForm(ABC):
@abstractmethod
def render(self) -> str: ...
class IStamp(ABC):
@abstractmethod
def imprint(self) -> str: ...
class ISignage(ABC):
@abstractmethod
def display(self) -> str: ...
# --- 2. 定義「供應商聯盟」的抽象介面 ---
class IThemeFactory(ABC):
@abstractmethod
def create_form(self) -> IForm: ...
@abstractmethod
def create_stamp(self) -> IStamp: ...
@abstractmethod
def create_signage(self) -> ISignage: ...
# --- 3. 實現「科技未來風」的具體產品與聯盟 ---
class TechnoForm(IForm):
def render(self) -> str: return "渲染 [科技感] 表單框"
class TechnoStamp(IStamp):
def imprint(self) -> str: return "蓋下 [雷射] 印章"
class TechnoSignage(ISignage):
def display(self) -> str: return "顯示 [全息投影] 路牌"
class TechnoFactory(IThemeFactory):
def create_form(self) -> IForm: return TechnoForm()
def create_stamp(self) -> IStamp: return TechnoStamp()
def create_signage(self) -> ISignage: return TechnoSignage()
# --- 4. 實現「溫暖木質風」的具體產品與聯盟 ---
class WoodForm(IForm):
def render(self) -> str: return "渲染 [木質紋理] 表單框"
class WoodStamp(IStamp):
def imprint(self) -> str: return "蓋下 [橡木] 印章"
class WoodSignage(ISignage):
def display(self) -> str: return "顯示 [雕刻] 路牌"
class WoodFactory(IThemeFactory):
def create_form(self) -> IForm: return WoodForm()
def create_stamp(self) -> IStamp: return WoodStamp()
def create_signage(self) -> ISignage: return WoodSignage()
# --- 5. 採用註冊模式的、可擴充的城市樣式服務 ---
class CityStyleService:
_registry: Dict[str, Type[IThemeFactory]] = {}
@classmethod
def register_style(cls, name: str, factory_cls: Type[IThemeFactory]):
cls._registry[name] = factory_cls
def get_factory(self, style: str) -> IThemeFactory:
factory_cls = self._registry.get(style)
if not factory_cls:
# 故障策略:明確拒絕未知風格的請求 (Fail Fast)。
raise ValueError(f"未知的風格: {style}")
return factory_cls()
# --- 初始註冊 ---
CityStyleService.register_style("techno", TechnoFactory)
CityStyleService.register_style("wood", WoodFactory)
# --- 6. 客戶端應用 ---
def apply_theme(factory: IThemeFactory):
"""
這個客戶端函式完全不知道現在是什麼風格,
它只知道如何使用一個「聯盟」來取得它需要的所有產品。
"""
form = factory.create_form()
stamp = factory.create_stamp()
signage = factory.create_signage()
print(form.render())
print(stamp.imprint())
print(signage.display())
# --- 驗收時間 ---
print("--- 啟動「科技未來風」主題 ---")
style_service = CityStyleService()
techno_alliance = style_service.get_factory("techno")
apply_theme(techno_alliance)
print("\n--- 一鍵切換至「溫暖木質風」主題 ---")
wood_alliance = style_service.get_factory("wood")
apply_theme(wood_alliance)
劇情收束:清晨五點,Mia 來到市中心廣場進行最後驗收。她只在總控制台下達了一道指令,全城的路牌、號碼牌、表單都完美切換。更重要的是,同一組參數化測試 (Parameterized Test),在傳入 techno
和 wood
兩種風格設定下都順利通過。這代表系統預期行為一致,僅表徵不同,完美證明了系統的健壯性。昨天的混亂,彷彿從未發生過。
想成為 Codetopia 的王牌供應商嗎?來試試這幾個挑戰吧!
新增供應商:市長又有新點子了!請新增一個「新古典風 (NeoClassic
)」供應商聯盟。你只需要新增 NeoClassic
相關的產品與聯盟類別,並加上一行 CityStyleService.register_style(...)
即可。你必須在不修改 CityStyleService
和 apply_theme
函式的前提下,完成這個新風格的擴充。
擴充產品線:
基礎:所有聯盟,現在都必須額外提供一項新產品:「印台 (IInkPad
)」。思考一下,這會對現有的程式碼造成什麼樣的衝擊?這正暴露了抽象工廠模式最核心的維護成本:家族擴編的陣痛。
延伸:還記得開場故事裡的「制服 (Uniform
)」嗎?請試著將它也納入產品家族中。
反模式紅旗 🚩:
聯盟內的 if:如果你在 TechnoFactory
的 create_form
方法裡,又看到 if (user_level == "VIP") return TechnoVIPForm();
,這聞起來是什麼味道?(提示:聯盟的職責是生產同一家族的產品,而不是在內部再做一次業務邏輯判斷)。
家族成員叛逃:如果 WoodFactory
跟你說:「抱歉,我們家就是做不出木頭的全息投影路牌」,導致 create_signage()
直接拋出 NotImplementedError
。這破壞了什麼?抽象工廠的核心承諾=家族齊備且可替換;NotImplementedError
代表承諾被破壞。
把聯盟做成 Singleton:為了方便,你把 TechnoFactory
變成了全域唯一的 Singleton。這會導致什麼問題?(提示:如果未來需要同時支援兩種主題,或是在測試時需要替換聯盟,會發生什麼事?)
今天,我們在微觀層面建立了供應商聯盟。如果把這個概念放大到整個城市架構:
這非常接近宏觀 MAS (多代理系統) 的概念。每一個具體的聯盟(TechnoFactory
)就像一個代理 (Agent),它向城市的黃頁服務 (Directory Facilitator, DF) 註冊自己的「能力」(我能提供科技未來風的全套產品)。當 Mia 需要某種風格時,她不是直接去找聯盟,而是去查詢黃頁,看看哪個代理宣告了這項能力。
在 EDA (事件驅動架構) 中,ThemeChanged
事件可以被廣播到事件總線上。一個或多個「樣式服務」監聽這個事件,然後動態地從設定中心或服務註冊中心,拉取對應風格的聯盟實例來使用。
若未來出現「可選」的家族成員(例如,只有某些聯盟提供高級印台),可以考慮引入「能力宣告 (Capability)」機制搭配預設的 Null 物件。但這通常也是一個警訊,可能代表你的家族邊界切分得還不夠穩定。
單品靠方法,成套找工廠;家族一致性,聯盟來擔當。
今天,我們學會了如何從 Day 3 的「櫃台分流」思維,升級到 Day 4 的「整套換裝」思維,用抽象工廠模式確保了 Codetopia 的視覺風格再也不會精神分裂。
但是,如果今天我們要蓋的不是一套風格,而是一棟極其複雜的摩天大樓呢?這棟大樓的施工流程是固定的(打地基 → 蓋鋼骨 → 裝玻璃帷幕),但每一步的具體材料和工法又可以替換(A 牌鋼骨 vs B 牌鋼骨)。這時,我們需要的就不是「換全套」,而是「分步施工」的智慧了。
明天,我們將請出都更署的流程總監。敬請期待 Day 5:都更署的秘密武器——Builder 模式搞定複雜物件的分步施工!
微觀 Class Diagram
┌─────────────────────────────────────────────────────────────────┐
│ 🔵 抽象介面層 (Interface Layer) │
└─────────────────────────────────────────────────────────────────┘
📋 [IThemeFactory] <<interface>> 📄 [IForm] <<interface>>
|-- createForm() : IForm 📮 [IStamp] <<interface>>
|-- createStamp() : IStamp 🪧 [ISignage] <<interface>>
|-- createSignage() : ISignage
│ │
│ implements │ implements
▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ 🟢 具體工廠層 (Factory Layer) │
└─────────────────────────────────────────────────────────────────┘
🏭 [TechnoFactory] 🏭 [WoodFactory]
|-- createForm() : TechnoForm |-- createForm() : WoodForm
|-- createStamp() : TechnoStamp |-- createStamp() : WoodStamp
|-- createSignage() : TechnoSignage |-- createSignage() : WoodSignage
│ creates │ creates
▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ 🟠 產品實作層 (Product Layer) │
└─────────────────────────────────────────────────────────────────┘
🔷 科技未來風家族 🔶 溫暖木質風家族
┌─────────────────┐ ┌─────────────────┐
│ TechnoForm │ │ WoodForm │
│ TechnoStamp │ │ WoodStamp │
│ TechnoSignage │ │ WoodSignage │
└─────────────────┘ └─────────────────┘
# 架構說明:
🔵 介面層:定義契約,確保一致性 🟢 工廠層:負責生產,保證家族完整
🟠 產品層:具體實作,風格統一 🔷🔶 家族:同風格產品組合
# 設計原則:
✅ 開閉原則:新增風格不修改既有程式碼
✅ 依賴反轉:高層依賴抽象,不依賴具體
✅ 單一職責:每個聯盟專注一種風格
中觀 Flow
👤 [Mia下指令: techno風格]
│
▼
🏛️ [城市樣式服務 CityStyleService]
│
▼
🔍 {查詢註冊表聯盟列表} ──→ 找到techno聯盟
│
▼
🏭 [實例化 TechnoFactory]
📋 [表單申請系統] ────1.請求Form────→ 🏛️ [樣式服務]
🪧 [路牌管理系統] ────1.請求Signage──→ 🏛️ [樣式服務]
📮 [印章管理系統] ────1.請求Stamp────→ 🏛️ [樣式服務]
🏛️ [樣式服務] ──2.使用工廠建立──→ 📄 [TechnoForm 科技感表單]
🏛️ [樣式服務] ──2.使用工廠建立──→ 🌐 [TechnoSignage 全息投影路牌]
🏛️ [樣式服務] ──2.使用工廠建立──→ ⚡ [TechnoStamp 雷射印章]
📄 [TechnoForm] ────3.回傳成品────→ 📋 [表單申請系統]
🌐 [TechnoSignage] ──3.回傳成品────→ 🪧 [路牌管理系統]
⚡ [TechnoStamp] ────3.回傳成品────→ 📮 [印章管理系統]
# 圖例說明:
👤 使用者操作 🏛️ 核心服務 🔍 決策點 🏭 工廠
📋🪧📮 應用系統 📄🌐⚡ 產品成品
# 讀圖口訣:應用端拿成品,不拿工廠。一個指令,全城換裝!