iT邦幫忙

2025 iThome 鐵人賽

DAY 4
0
Software Development

Codetopia 新手日記:設計模式與原則的 30 天學習之旅系列 第 4

Day 4:供應商聯盟的智慧——Abstract Factory 一次搞定成套風格!

  • 分享至 

  • xImage
  •  

Codetopia 創城記 (4)|供應商聯盟的智慧——Abstract Factory 一次搞定成套風格!

IThome 鐵人賽 設計模式 Abstract Factory Creational Patterns Codetopia

1. 今日熱點 (故事開場 & 痛點)

Codetopia 的市長辦公室(就是那個獨一無二的 Singleton!)頒布了一項新命令:「城市視覺風格週」正式啟動!要求全市所有公共設施,必須在一夜之間,換上風格完全一致的「科技未來風」主題。

這下可忙壞了採購科的專員 Mia。她的辦公桌被四家不同供應商的業務代表團團圍住。

「Mia 小姐,我們家的路牌,絕對是賽博龐克首選!」

「我們家的號碼牌,極簡線條,科技感十足!」

「這是我們的電子表單框樣式,參考一下?」

「還有我們的制服!絕對跟得上潮流!」

Mia 一個頭兩個大。她發現,每一項單品看起來都很棒,但當她試圖將 A 家的路牌、B 家的表單和 C 家的印章湊在一起時… 畫面簡直慘不忍睹。供應商們只保證自家產品的品質,卻沒有一個機制能保證他們彼此之間能「搭得起來」。

危機警報在 Mia 腦中大響 🚨。如果她像昨天一樣,用 Factory Method 來逐項分流櫃台,那明早的頭條新聞絕對是:「Codetopia 市容災難:未來風路牌配上復古木質風印章,市民美感集體崩潰!」

這一次,問題的核心不再是「如何生產單一物件」,而是「如何確保一整組物件的風格一致性」。這就是產品家族的挑戰與價值所在。

2. 笑中帶淚 (反例/壞味道)

在 Mia 找到解決方案前,整個採購流程簡直就是一場「拼裝車大賽」,充滿了各種令人噴飯的壞味道:

  • 「拼裝城市」風 🚗:路牌是 A 廠的,號碼牌是 B 廠的,表單是 C 廠的。每個看起來都不錯,組合起來卻像一輛用膠帶黏起來的報廢車,跑起來隨時會散架。

  • 「風格判斷 if-else」散落全城 🤧:更可怕的是,為了「看起來」一致,程式碼裡到處都是 if (style == "techno") { set_techno_border(); } else if (style == "wood") { set_wood_texture(); }。從申請表單的邊框,到承辦員制服的鈕扣,每個小元件都在獨立進行一次風格判斷。這根本不是城市規劃,這是全民大家來找碴!

  • 「下週就回不去」的單行道 😭:市長突然又說:「下週我們改走溫暖木質風!」Mia 眼前一黑。這意味著她和工程師團隊得把整個城市所有程式碼翻過來,把上百個 techno 字串手動換成 wood。萬一漏掉一個,災難重演。

[拍案] 說白了,這一切混亂的根源就在於:所謂的「風格一致性」,全靠開發人員用肉眼來驗收,系統本身沒有任何機制來保證一個「家族」的完整性。

3. 王牌出手 (核心觀念/何時用/不適用)

為了解決這場「混搭災難」,Mia 搬出了今天的王牌工法——抽象工廠模式 (Abstract Factory)

這同樣是一種創生型模式,但它的野心比 Factory Method 大得多。它的核心思想是:

提供一個介面,用於建立一系列相關或相互依賴的物件(一個產品家族),而無需指定它們具體的類別。

用 Codetopia 的話來說,就是成立一個「供應商聯盟 (ThemeFactory)」!

  • 我們不直接跟單一的「路牌廠」或「制服廠」打交道。

  • 我們只跟「科技未來風聯盟」或「溫暖木質風聯盟」簽約。

  • 一旦選定了聯盟(具體的工廠),這個聯盟就必須負責提供一整套風格相容的路牌、印章、表單...等等。

這樣一來,我們就把「確保一致性」的責任,從 Mia 和開發者身上,轉移到了聯盟這個抽象層級。

何時用 (When to Use)

  • 當你需要「以家族為單位」工作時:你的系統需要一整組產品,而且這些產品必須來自同一個「家族」(例如:同一種視覺主題、同一個作業系統的 UI 元件)。

  • 當你希望系統獨立於其產品的建立、組合和表示時:客戶端程式碼只依賴抽象介面,完全不知道背後是哪個聯盟在生產。

  • 當你想在工廠層級強制執行一致性約束時:一致性由聯盟來保證,而不是靠客戶端的人工檢查或 if-else

何時不要用 (When NOT to Use) ⚠️

  • 你只需要一個產品,且它與其他產品沒有關聯:殺雞焉用牛刀。如果只是要生產昨天的 Clerk,Factory Method 就綽綽有餘了。

  • 產品家族內的物件之間,沒有太強的一致性約束:如果路牌和印章的風格本來就不需要搭配,那硬是把它們綁在同一個聯盟裡,反而會降低彈性。

  • 產品家族的介面不穩定,經常需要增加新產品:每次在家族中增加一個新產品(例如從今天起,所有風格都要額外提供「印台」),你就必須修改抽象工廠的介面,這會導致所有實作該介面的具體聯盟都需要跟著修改,違反開閉原則。

[旁批] 過渡解:減痛替代方案

如果你的產品家族還在快速演化,邊界未定,直接上 Abstract Factory 可能會讓你因為頻繁修改介面而痛苦不堪。這時可以考慮:

  1. 先用 Factory Method + 組態檔:用多個工廠方法分別生產單品,靠外部組態檔來約束它們的組合關係。

  2. 搭配橋接模式 (Bridge Pattern):將「風格」這個維度與「功能」維度分離,讓它們可以獨立變化,待風格穩定後再考慮用 Abstract Factory 固化下來。

4. 導播切景 (表格+兩張 Mermaid)

導播,鏡頭拉一下!讓我們用三種不同的焦段,來看看這個供應商聯盟是怎麼運作的。

[旁批] 這區塊技術含量比較高,就像是總設計師的施工小抄。看不懂?沒關係,記得結論就好:「以前東拼西湊,聯盟一下,全都對齊!

視角 觀念/模式 在 Codetopia 的說法
微觀 (GoF) Abstract Factory 供應商聯盟 (ThemeFactory) 產出成套的 Form, Stamp, Signage 家族
中觀 (訊息/事件) 事件觸發,動態更換 Provider 「主題切換 (ThemeChanged)」事件,觸發「樣式服務」更換聯盟實例
宏觀 (MAS) 代理透過黃頁 (DF) 查詢能力 採購單位向「黃頁」查詢:哪個聯盟具備「科技未來風」的生產能力?

契約測試:每個聯盟都必須遵守 IThemeFactory 的契約,同時實作 IForm, IStamp, ISignage 三件產品,否則在整合測試階段就會立刻被標記為違約,直接 Fail!這就把「一致性」從人眼判斷,升級為機器自動驗證。

微觀:供應商聯盟的組織結構 (Class Diagram)

這張圖展示了聯盟的契約(抽象工廠),以及兩個實際的供應商(具體聯盟)如何履行合約。

https://ithelp.ithome.com.tw/upload/images/20250919/201785001L7tPI002Z.png

中觀:一夜變裝的執行流程 (Flowchart)

這張圖展示了 Mia 如何透過一個統一的「城市樣式服務」,僅用一個指令就完成全城的風格切換。

https://ithelp.ithome.com.tw/upload/images/20250919/20178500CXmhaLk8dh.png

[旁批] 注意看流程!所有請求都由樣式服務統一調度。

讀圖口訣:「應用端拿成品,不拿工廠。

這就確保了風格決策權集中,避免了判斷邏輯散落全城的災難。

5. 最小實作 (程式碼範例)

在動手實作前,先確立我們的故障處理策略。當一個未知的風格被請求時,有兩種主流做法:回退(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),在傳入 technowood 兩種風格設定下都順利通過。這代表系統預期行為一致,僅表徵不同,完美證明了系統的健壯性。昨天的混亂,彷彿從未發生過。

6. 鄉民出題 (動手+反模式紅旗)

想成為 Codetopia 的王牌供應商嗎?來試試這幾個挑戰吧!

  1. 新增供應商:市長又有新點子了!請新增一個「新古典風 (NeoClassic)」供應商聯盟。你只需要新增 NeoClassic 相關的產品與聯盟類別,並加上一行 CityStyleService.register_style(...) 即可。你必須在不修改 CityStyleServiceapply_theme 函式的前提下,完成這個新風格的擴充。

  2. 擴充產品線

    • 基礎:所有聯盟,現在都必須額外提供一項新產品:「印台 (IInkPad)」。思考一下,這會對現有的程式碼造成什麼樣的衝擊?這正暴露了抽象工廠模式最核心的維護成本:家族擴編的陣痛。

    • 延伸:還記得開場故事裡的「制服 (Uniform)」嗎?請試著將它也納入產品家族中。

  3. 反模式紅旗 🚩:

    • 聯盟內的 if:如果你在 TechnoFactorycreate_form 方法裡,又看到 if (user_level == "VIP") return TechnoVIPForm();,這聞起來是什麼味道?(提示:聯盟的職責是生產同一家族的產品,而不是在內部再做一次業務邏輯判斷)。

    • 家族成員叛逃:如果 WoodFactory 跟你說:「抱歉,我們家就是做不出木頭的全息投影路牌」,導致 create_signage() 直接拋出 NotImplementedError。這破壞了什麼?抽象工廠的核心承諾=家族齊備且可替換;NotImplementedError 代表承諾被破壞。

    • 把聯盟做成 Singleton:為了方便,你把 TechnoFactory 變成了全域唯一的 Singleton。這會導致什麼問題?(提示:如果未來需要同時支援兩種主題,或是在測試時需要替換聯盟,會發生什麼事?)

7. 城市望遠鏡 (進階視野)

今天,我們在微觀層面建立了供應商聯盟。如果把這個概念放大到整個城市架構:

  • 這非常接近宏觀 MAS (多代理系統) 的概念。每一個具體的聯盟(TechnoFactory)就像一個代理 (Agent),它向城市的黃頁服務 (Directory Facilitator, DF) 註冊自己的「能力」(我能提供科技未來風的全套產品)。當 Mia 需要某種風格時,她不是直接去找聯盟,而是去查詢黃頁,看看哪個代理宣告了這項能力。

  • EDA (事件驅動架構) 中,ThemeChanged 事件可以被廣播到事件總線上。一個或多個「樣式服務」監聽這個事件,然後動態地從設定中心或服務註冊中心,拉取對應風格的聯盟實例來使用。

  • 若未來出現「可選」的家族成員(例如,只有某些聯盟提供高級印台),可以考慮引入「能力宣告 (Capability)」機制搭配預設的 Null 物件。但這通常也是一個警訊,可能代表你的家族邊界切分得還不夠穩定。

8. 結語 & 預告

單品靠方法,成套找工廠;家族一致性,聯盟來擔當。

今天,我們學會了如何從 Day 3 的「櫃台分流」思維,升級到 Day 4 的「整套換裝」思維,用抽象工廠模式確保了 Codetopia 的視覺風格再也不會精神分裂。

但是,如果今天我們要蓋的不是一套風格,而是一棟極其複雜的摩天大樓呢?這棟大樓的施工流程是固定的(打地基 → 蓋鋼骨 → 裝玻璃帷幕),但每一步的具體材料和工法又可以替換(A 牌鋼骨 vs B 牌鋼骨)。這時,我們需要的就不是「換全套」,而是「分步施工」的智慧了。

明天,我們將請出都更署的流程總監。敬請期待 Day 5:都更署的秘密武器——Builder 模式搞定複雜物件的分步施工!


附錄:ASCII 版圖示

微觀 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.回傳成品────→ 📮 [印章管理系統]

# 圖例說明:
👤 使用者操作    🏛️ 核心服務    🔍 決策點    🏭 工廠
📋🪧📮 應用系統   📄🌐⚡ 產品成品

# 讀圖口訣:應用端拿成品,不拿工廠。一個指令,全城換裝!

上一篇
Day 3:市民服務櫃台的秘密武器——Factory Method 搞定千變萬化的申請單!
下一篇
Day 5:都更署的秘密武器——Builder 模式搞定複雜物件的分步施工!
系列文
Codetopia 新手日記:設計模式與原則的 30 天學習之旅6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言