iT邦幫忙

2025 iThome 鐵人賽

DAY 11
0
Software Development

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

Day 11:市民服務「一道門」搞定!Facade 模式的簡潔藝術

  • 分享至 

  • xImage
  •  

Codetopia 創城記 (11)|市民服務「一道門」搞定!Facade 模式的簡潔藝術

IThome 鐵人賽 設計模式 Facade Codetopia

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

Codetopia 晚會的喧囂剛剛散去,清晨的第一縷陽光還沒照進辦公室,市政廳前台主管 Ava 的電話就已經被打爆了。媒體、交通單位、活動承辦商… 合作夥伴們像一群迷路的羔羊,在電話那頭哀嚎遍野。

「Ava 主管,我想發個公告,到底要先呼叫『內容組包 API』還是先跑『安全簽章』?」

「那個… LED 看板的驅動 Vendor-A 說他們要維護,我們是不是得改接 Vendor-B 的 API?」

「天啊,我只是想把三則短訊打包成一個夜間套餐,為什麼要我懂你們的巢狀結構、加密順序和通路派遣…」

砰!Ava 揉了揉太陽穴。她意識到,我們不能再讓合作夥伴拿著一份「IKEA 家具組裝說明書」來跟市政廳打交道了。他們需要的不是一堆零件,而是一個開箱即用的櫃子。

於是,Ava 拍板定案:啟動「一站式市民快辦 API」專案!在市政廳內部拉起一道俐落的門面,把所有複雜的流程——組包、加密、簽章、多通路派遣——全都藏在門後。對外,只留一個清爽無比的窗口。

【驗收標準】 (Ava 對技術團隊下的軍令狀)

  • Given:三則公告打包成一個 NightPack,安全政策為「先加密再簽章」,目標通路是 LED 看板和 App 推播。

  • When:外部合作夥伴只需呼叫一個方法:CivicBroadcastFacade.publish(...)

  • Then:Facade 內部要能完美協調組合 (Composite)裝飾 (Decorator)橋接 (Bridge) 三大子系統。最重要的是,當 LED 供應商從 Vendor-A 換成 Vendor-B 時,外部夥伴的程式碼一個字都不用改

2. 術語卡 🧭

  • Facade (外觀/門面):為一群複雜的子系統提供一個單一、簡潔的統一入口。它自己不處理業務,只做「轉接」和「協調」。

  • Composite (組合):將物件組合成樹狀結構,讓客戶端能以一致的方式處理單一物件和物件群組。(Day 9 的老朋友)

  • Decorator (裝飾):在不改變物件結構的情況下,動態地為物件增加新功能。(Day 10 的老朋友)

  • Bridge (橋接):將抽象部分與它的實現部分分離,使它們都可以獨立地變化。(未來會遇到的重要角色)

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

讓我們把時間倒回專案啟動前的那個混亂早晨。鏡頭轉到合作單位一位菜鳥工程師 Andy 身上,他的螢幕上正是這段令人落淚的「五連招」腳本:

# ❌ 反例:合作夥伴的工程師 Andy 必須親手編排所有內部細節

# (第一招:拜託內容組包系統)
pack = build_night_pack()
# (第二招:手動展開巢狀結構...為什麼我要懂這個?)
payloads = expand_to_leafs(pack)
# (第三招:呼叫安全模組 A 進行加密)
payloads = encrypt(payloads)
# (第四招:呼叫安全模組 B 進行簽章...順序反了會怎樣?天曉得!)
payloads = sign(payloads)
# (第五招:自己寫 if-else 切換通路...供應商一換我就得加班)
for p in payloads:
    if channel == "LED":
        led_driver_vendor_A.send(p) # 啊,聽說要換 Vendor B 了...
    elif channel == "APP":
        app_driver.send(p)

# Andy (內心獨白): 「我只是想發個公告,為什麼感覺像在手動組裝火箭?」
# 任何一個子系統的微小改動,都會讓 Andy 的週末泡湯。

災難現場分析

  • 高度耦合:客戶端 Andy 必須知道所有子系統的名稱、順序,甚至是供應商的細節。

  • 脆弱不堪:市政廳內部任何一次重構或升級,都可能引發外部合作夥伴的連鎖性崩潰。

  • 重複的惡夢:每個合作夥伴都得複製貼上這段樣板程式碼,形成一片廣大的「技術債沼澤」。

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

一句話概括:Facade (外觀/門面) 模式就是「把複雜留在門後,給世界一個微笑」。它就像一家五星級飯店的萬能前台,你只需要說「我要去機場」,前台就會在幕後協調門僮、禮賓部、計程車調度中心,你完全不必操心這些細節。

何時用 (When to Use) ✅

  • 簡化介面:當你有一堆複雜的子系統,但只想對外提供一組固定、簡單的操作時。

  • 建立防腐層 (Anti-corruption Layer):當你想隔離一個老舊、混亂或第三方系統時,用 Facade 包起來,保護你的新系統不被污染。

  • 分層解耦:當你希望系統的外部介面保持穩定,而內部可以自由地進行重構和演進時。Facade 就是那道穩固的防火牆。

何時不要用 (When NOT to Use) ⛔

  • 只是想翻譯介面:如果只是 A 介面轉 B 介面,沒有簡化流程的需求,考慮更輕巧的 Adapter (轉接器) 模式。

  • 想分離不同維度的變化:如果你的問題是「形狀」和「顏色」兩種屬性在交叉組合,導致類別爆炸,那應該考慮 Bridge (橋接) 模式。

  • 需要控制存取權限:如果你想做的是權限控管、延遲載入或遠端代理,那主角應該是 Proxy (代理人) 模式。

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

導播,鏡頭拉一下!讓我們用三個不同的縮放層級,看看 Ava 的設計在 Codetopia 的架構中是如何定位的。

視角 觀念/模式 在 Codetopia 的說法
微觀 (GoF) Facade 對多個 Subsystems 提供單一 API CivicBroadcastFacade 負責協調 ComposerPolicyGateway 三大局處。
中觀 (EIP/EDA) Gateway / Aggregator / Translator 「市民快辦 API Gateway」作為統一入口,內部透過事件路由與訊息轉換,完成複雜任務。
宏觀 (MAS) FrontDeskAgent 協調 Specialist Agents 「前台代理人」接到任務後,查詢黃頁服務(DF),找到「公告聚合代理」、「安全策略代理」、「通路派遣代理」並指揮它們協同作戰。

Mermaid|類圖 (微觀 GoF 結構)

https://ithelp.ithome.com.tw/upload/images/20250925/20178500FHZ8JLKkjd.png

Mermaid|時序圖 (中/宏觀流程)

https://ithelp.ithome.com.tw/upload/images/20250925/20178500Kp3sTEfcUb.png

6. 最小實作 (正解|精修版)

現在,讓我們看看 Ava 的團隊是如何將這個優雅的設計化為可落地的工程現實。注意看,Facade 本身依然只負責協調委派,但它的契約變得更強固了。

from typing import Protocol, List, Dict, Any, Tuple
import time, random

# --- 核心型別定義,作為公開契約 ---
class Payload(dict): ...
Ack = Tuple[bool, Dict[str, Any]]  # (ok, meta) 以 type alias 取代繼承 typing 型別
class RetryableError(Exception): ...

# --- 子系統的介面 (Protocols) ---
class Policy(Protocol):
    def apply(self, items: List[Payload]) -> List[Payload]: ...

class Composer(Protocol):
    def compose(self, pack: Any) -> List[Payload]: ...

class Gateway(Protocol):
    def dispatch(self, items: List[Payload], channels: List[str], *, idempotency_key: str) -> Ack: ...

# --- 統一的錯誤型別 ---
class FacadeError(Exception):
    def __init__(self, code: str, message: str, cause: Any = None):
        super().__init__(message)
        self.code, self.cause = code, cause

# --- 我們的主角:堅固的 Facade ---
class CivicBroadcastFacade:
    def __init__(self, composer: Composer, policy: Policy, gateway: Gateway):
        # 依賴注入!這些專家都是從外部請來的。
        self.composer = composer  # Day 9 的老朋友 (Composite) 負責組包
        self.policy = policy      # Day 10 的老朋友 (Decorator) 負責加工
        self.gateway  = gateway   # 未來之星 (Bridge) 負責派遣

    def publish(self, pack: Any, channels: List[str], *, idempotency_key: str) -> Ack:
        # 觀察點:在這裡啟動一個 trace span
        try:
            items = self.composer.compose(pack)
            staged = self.policy.apply(items)
            ok, meta = self.gateway.dispatch(
                staged, channels, idempotency_key=idempotency_key
            )

            if not ok:
                raise FacadeError(
                    code="DISPATCH_FAILED",
                    message="One or more channels failed",
                    cause=meta.get("errors")
                )
            return (ok, meta)

        except Exception as e:
            # 觀察點:記錄結構化日誌
            if not isinstance(e, FacadeError):
                raise FacadeError(code="INTERNAL_ERROR", message=str(e)) from e
            raise e

7. API 契約 (最小公開規格)

一份穩定的 Facade,必然伴隨著一份清晰的 API 契約。

# OpenAPI (excerpt)
paths:
  /civic/publish:
    post:
      summary: Publish a NightPack to multiple channels via Facade
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [pack, channels, idempotencyKey]
              properties:
                pack: { $ref: '#/components/schemas/NightPack' }
                channels: { type: array, items: { type: string, enum: [LED, APP, WEBWALL] } }
                idempotencyKey: { type: string, minLength: 8 }
      responses:
        "200":
          description: Ack
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  meta: { type: object }
        "4XX":
          description: FacadeError
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FacadeError'
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FacadeError'
components:
  schemas:
    FacadeError:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, traceId]
          properties:
            code: { type: string, enum: [BAD_REQUEST, DISPATCH_FAILED, OVER_CAPACITY, INTERNAL_ERROR] }
            message: { type: string }
            traceId: { type: string, format: uuid }

版控策略:路徑固定、以Header 版號X-Api-Version: 2025-09-01)或 /v2 演進,向後相容 6 個月後才公告移除期。

8. 反模式紅旗 🚩

Facade 雖然好用,但一不小心就會養出怪獸。看到以下這些「壞味道」,就該亮起紅旗了:

  • 🚩 萬能的上帝 Facade:什麼邏輯都往 Facade 裡塞,最後它變成了另一個難以維護的巨大泥球。記住,Facade 是協調者,不是實幹家。

  • 🚩 洩漏的抽象:Facade 的方法簽名或回傳值中,出現了內部子系統的特定類型。

  • 🚩 薄如紙的門面:Facade 只是簡單地將內部方法一對一轉發,完全沒有簡化任何流程。

  • 🚩 外部的順序依賴:如果外部依然需要先呼叫 facade.stepA() 再呼叫 facade.stepB(),那這個 Facade 就失去了「一站式」的意義。

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

從城市的制高點俯瞰,Facade 模式的 DNA 可以在更宏大的架構中找到它的影子,但請注意,它們角色不同,不能劃上等號。

總設計師的筆記:Facade vs. API Gateway

名稱 解決層級 典型職責 不是它的事
Facade 程式內部物件層 對多個子系統提供簡化介面協作編排 認證、流量限制、跨服務路由
API Gateway 系統邊界層 驗證、路由、聚合、轉換、風控、流量治理 內部物件協作細節

總設計師的筆記:Facade vs. Mediator

Facade 對外簡化既有子系統的使用;Mediator 協調同級物件之間的互動以避免彼此直接耦合。本文的 CivicBroadcastFacade 不擔任業務規則的中心(那會是 Mediator 的職責),而是把固定順序的跨子系統流程包在門內,對外暴露最小介面。

模式 關注點 對象 對外介面
Facade 對外簡化 異質子系統 穩定、極簡
Mediator 物件互動 同層同事物件 事件式 / 回呼

10. 跨通路原子性說明

預設情況下,publish()best-effort 模式運作,這意味著每個通路(LED、APP)的發送是獨立的,一個失敗不影響另一個。如果業務需要 all-or-nothing(全成功或全失敗),則需啟用 dispatchMode="atomic",這會由 ChannelGateway 內部啟用更複雜的二階段提交或補償交易(Saga)模式來保證最終一致性。

11. 效能與韌性設計

  • 併發度ChannelGateway.dispatch()max_inflight_per_channel=N 控制單通路併發;超過則排隊。

  • 重試退避:對可重試錯誤(5xx/網路)採指數退避+抖動,上限 R 次;對非可重試錯誤(4xx/合規)不重試

  • 背壓:當隊列超過 Q 時,Facade 回 429 Too Many Requests,錯誤碼 OVER_CAPACITY,避免雪崩。

  • 批量派送:相同通路可批量聚合(例如 LED 允許一次送多筆),減少往返。

調參建議(起步):N=8, R=3, Q=1000;觀測 p95 latency / 成功率 / 429 比例 後滾動調整。

12. ✅ 回到現場 (同一組驗收通過)

讓我們回到開頭的驗收場景:

  • Given:還是那個 NightPack。但現在,ChannelGateway 內部的 LED 驅動悄悄從 Vendor-A 換成了 Vendor-B。

  • When:合作夥伴 Andy 的程式碼完全沒動,依然是那一行清爽的呼叫:facade.publish(pack, channels=["LED", "APP"], idempotency_key="req-123")

  • Then:公告被正確地組包、上鍊、並成功發送到了 LED 看板和 App 上。Andy 甚至不知道供應商換了。Ava 滿意地點點頭,端起了她那杯應得的咖啡。驗收通過!

13. 測試指北

要確保這道漂亮的門面堅固可靠,我們的測試策略也必須跟著升級:

  • 契約測試 (Contract Testing):驗證 API 的成功錯誤回傳格式是否符合 OpenAPI 規格。

  • 冪等性測試:使用相同的 idempotency_key 重複呼叫 publish(),驗證 gateway.dispatch 只被實際執行了一次

  • 模擬與注入 (Mocking & Injection):注入假的 FakeComposerFakePolicyFakeGateway,驗證 Facade 是否以正確的順序、用正確的參數呼叫了協作者。

  • 抗變測試 (Mutation Testing):故意替換 ChannelGateway 內部的驅動,重新執行契約測試,確保外部行為不變。

14. 可觀測性落地 (指標、日誌與追蹤)

  • Metrics (建議最少)

    • facade_publish_total{channel, vendor, outcome}

    • facade_dispatch_latency_ms{channel, vendor} (p50/p95/p99)

    • facade_retry_total{channel, vendor, reason}

    • idempotency_hits_total

  • Logs (結構化欄位)traceId, idempotencyKey, channels[], vendorMap, retryCount, errorCode.

  • Traces:一個 root span (Facade.publish),子 span:compose / apply(policy) / dispatch(channel=...)

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

總設計師,藍圖已經就位,現在輪到你來為這座城市添磚加瓦了!

  1. 小試身手:請為 ChannelGateway 新增一個 WebWall (網路電視牆) 通路。你的目標是,在不修改 CivicBroadcastFacade 任何一行程式碼的前提下完成擴充。

  2. 責任思辨:目前公告的「摘要模式」是在 NoticeComposer 中完成的。有人提議,不如在 Policy 中新增一個 SummaryPolicy 來做。請比較這兩種做法在職責劃分上的優劣。

  3. 靈魂拷問 (小投票):某天,一個「超級用戶」希望能自訂發送流程。你會選擇:

    • A: 為 Facade 新增一個 publishWithCustomSteps(...) 的「後門」方法。

    • B: 堅決捍衛 Facade 的簡潔性,拒絕這個需求。

      請在留言區留下你的選擇 (A/B) 和一句話理由!

  4. 回魂一問:請回顧「笑中帶淚」中 Andy 的腳本。現在,請你只用一行 facade.publish(...) 呼叫來改寫它,徹底拯救 Andy 的週末。

16. 結語 & 預告

二十字摘要複雜留門內,對外招式簡。門後勤換將,門外不知曉。

明日預告:全城的圖示、字型、共享單車… 數量一多,記憶體就拉警報!明天,我們來看看 Flyweight (享元) 模式,如何用「共享經濟」的思路,為 Codetopia 的 RAM 減負!


17. 附錄:ASCII 版圖示

為了確保在不支援 Mermaid 渲染的環境中也能正常閱讀,以下提供文中圖表的 ASCII 替代版本:

類圖 (微觀 GoF 結構)

+---------------------------+      聚合       +--------------------+
|   CivicBroadcastFacade    |<>------------->|   NoticeComposer    |
+---------------------------+                +--------------------+
| +publish(pack, channels,  |      聚合       +--------------------+
|  idempotency_key): Ack    |<>------------->|      Policy         |
+---------------------------+                +--------------------+
                |                            +--------------------+
                | 聚合                        |   ChannelGateway    |
                +--------------------------->+--------------------+

時序圖 (中/宏觀流程)

    合作夥伴Andy         市民快辦Facade         內容組包處         安全策略局         通路派遣中心
        |                     |                   |                |                 |
        |--publish()--------->|                   |                |                 |
        |                     |                   |                |                 |
        |                     |---compose()------>|                |                 |
        |                     |<--payloads[]------|                |                 |
        |                     |                   |                |                 |
        |                     |---apply()----------------------->  |                 |
        |                     |<--secured_payloads[]---------------|                 |
        |                     |                   |                |                 |
        |                     |---dispatch()---------------------------->|           |
        |<--ack---------------|                   |                |<-----------------|
        |                     |                   |                |                 |

Facade設計結構圖

   +-------------------------------------------------------+
   |                                                       |
   |    +-----------+  +-----------+  +--------------+     |
   |    |           |  |           |  |              |     |
   |    | Composer  |  |  Policy   |  |   Gateway    |     |
-->| F  | (組合子系統) |  | (策略子系統) |  | (通路子系統)  |     |
   | A  |           |  |           |  |              |     |
   | C  +-----------+  +-----------+  +--------------+     |
   | A                                                     |
   | D      +-----------------------------------+          |
   | E      |                                   |          |
-->|       |   協調與流程控制 (只做轉接不做業務)   |          |
   |        |                                   |          |
   |        +-----------------------------------+          |
   |                                                       |
   +-------------------------------------------------------+

上一篇
Day 10:裝修加料——Decorator:不動原始物,疊上新能力
下一篇
Day 12:Flyweight:城市資產共享中心——一張圖示,千萬位置
系列文
Codetopia 新手日記:設計模式與原則的 30 天學習之旅12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言