Codetopia 科技藝術節第二天,人潮擠爆了中央廣場!市長辦公室為了即時疏導人流,下令要將一份緊急的「城市公告」(包含交通快訊、活動播報、天氣預警)同步送到全城各式各樣的顯示設備上。
這下可忙壞了資訊局的菜鳥工程師 Andy。你看,眼前有一整排五花八門的設備等著他處理:老舊的 LED 路邊顯示板、剛換裝的 LCD 觸控面板、官網的即時滾動條,還有市民手機裡的 App 推播。
「天啊…」Andy 哀嚎著。他發現每增加一台新設備,就得為每一種公告類型(交通、活動、天氣)複製貼上一整輪的 if/else
判斷,甚至還要為了不同廠商的 SDK、不同的語系樣式,再額外建立好幾個類別來硬湊。
昨天才剛接好的 IncidentBus 事件總線雖然穩定地把事件送了過來,但 「要顯示什麼」(公告的內容語意) 跟 「怎麼顯示」(設備的驅動細節) 這兩件事,像兩條濕透的麻繩一樣,死死地糾纏在一起。開發和測試的進度,就這樣被這場「組合爆炸」給徹底拖垮了。💥
【驗收標準】
Given:一份「交通速報」公告,以及一台由 Vendor-A 廠商提供的 LED 顯示板。
When:市府突然更換了新的供應商 Vendor-B,並且緊急追加了「App 推播」這個新通路。
Then:在完全不修改任何現有公告類別的前提下,只需替換或新增對應的「驅動程式」,所有公告就能照常送達,並順利通過驗收。
Bridge (GoF):將抽象 (例如公告的語意) 與實作 (例如輸出設備的驅動) 徹底分離,讓這兩個維度可以各自獨立擴充,互不干擾。
Port/Driver (六角形架構):抽象就是應用程式核心的 Port;實作就是外部溝通的具體 Adapter/Driver。
Andy 為了快速交差,決定使出蠻力。他心想:「不就是公告類型 × 裝置驅動 × 語系嗎?我全寫出來就是了!」於是,他的程式碼裡出現了一片壯觀的類別灌木叢:
TrafficAlertOnLedZh
, TrafficAlertOnLedEn
, TrafficAlertOnLcdZh
…
EventPromoOnWebZh
, EventPromoOnAppEn
…
(旁白:「孩子,你這是交叉繁殖,不是軟體開發啊!」)
這種寫法立刻引發了災難。每當市長早上喝咖啡時突發奇想,要加一個新的顯示設備或支援一種新語系,Andy 就得含淚擴增一整排的新類別。公告的格式邏輯與設備的通訊細節緊緊糾纏,測試案例數量呈指數級增長,重用性?那是什麼,能吃嗎?
# 壞味道:語意(公告)與設備/供應商細節糾纏在一起,if/elif 暴增
class NoticeService:
def publish_traffic_alert(self, msg: str, device: str, vendor: str, locale: str):
payload = {}
# 糟糕!公告的語系樣式跟具體設備綁死了
if device == "LED":
if vendor == "A":
payload = {"title": "交通速報" if locale == "zh" else "Traffic Alert",
"body": msg, "style": "bold-16x8"}
self._send_led_vendor_a(payload)
elif vendor == "B":
# 換個廠商,程式碼就要跟著改
payload = {"title": "交通速報" if locale == "zh" else "Traffic Alert",
"body": msg, "style": "bold-16x8"}
self._send_led_vendor_b(payload)
elif device == "APP":
payload = {"title": "交通速報" if locale == "zh" else "Traffic Alert",
"body": msg, "priority": "high"}
self._send_app_push(payload)
# ... 再加 LCD、Web、更多 vendor → if/elif 這棵樹就等著長成神木吧
# 下面全是通訊細節(實作),跟公告語意混在一起
def _send_led_vendor_a(self, payload: dict): print(f"[LED Vendor A] Sending: {payload}")
def _send_led_vendor_b(self, payload: dict): print(f"[LED Vendor B] Sending: {payload}")
def _send_app_push(self, payload: dict): print(f"[App Push] Sending: {payload}")
# 使用:每新增一種設備或換 vendor,就得回來修改 NoticeService 的程式碼
svc = NoticeService()
svc.publish_traffic_alert("X1 路口壅塞", device="LED", vendor="A", locale="zh")
問題點 💔
組合爆炸:類別的成長方向是「公告 × 設備/vendor × 語系」的交叉座標。
測試地獄:需要覆蓋大量的 if/elif
路徑;更換供應商這種小事,竟然會牽動核心的公告邏輯。
就在 Andy 瀕臨崩潰時,總設計師悄然現身,遞給他一張設計圖,上面只有一個詞:Bridge (橋接模式)。他只留下設計圖就退到人群裡,把解法的舞台交還給 Andy。
核心觀念 💡:這座「橋」的精髓,就是把**「要說什麼」(公告的抽象)** 與 「怎麼說」(設備驅動的實作) 拆解到兩條完全獨立的維度上。抽象端(Notice
)只負責組織訊息內容與樣式;實作端(DisplayDriver
)則專心處理與 LED/LCD/Web/App 的通訊細節。
這才是優雅的解法——從此兩軸各自演進、互不牽連。這正是 Bridge 模式在我們 Codetopia 藍圖裡的定位:維度分離,終結組合爆炸。從數學直覺來看,它讓類別的數量從 O(公告種類 × 設備種類) 的乘法關係,降低為 O(公告種類 + 設備種類) 的加法關係。
當同一個抽象概念需要有多種實作,而且兩者都可能獨立擴充時(例如:跨平台的 UI 渲染、支援多種資料庫的儲存庫、不同傳輸協定的訊息發送器)。
當系統存在兩條(或更多)正交的變化維度時(例如:公告種類 vs. 輸出設備)。
如果只有一條維度會變化(例如只是想切換不同的排序演算法),那麼更簡單的 Strategy (策略模式) 就足夠了。
如果只是要解決兩個既有介面的名稱或參數不匹配問題,昨天的 Adapter (轉接器模式) 更對症下藥。
如果你的需求是一整個「產品家族」要能被成套替換(例如 UI 的 Modern 風格與 Classic 風格),那麼 Abstract Factory (抽象工廠模式) 會更貼切。
導播,鏡頭拉一下!讓我們從三個不同的尺度,來看看這座優雅的「橋」是怎麼搭建起來的。
視角 | 觀念/模式 | 在 Codetopia 的說法 |
---|---|---|
微觀 (GoF) | Abstraction ↔ Implementor | 公告 ↔ 顯示驅動程式 |
中觀 (EIP/EDA/Actor) | Semantic vs. Channel Decoupling | 事件語意 vs. 傳輸通道解耦 |
宏觀 (MAS) | Agent Collaboration | 公告代理 ↔ 驅動代理協作 |
這張圖清楚地展示了「公告」與「顯示驅動」是如何透過 bridge
連結,同時又能在各自的繼承體系中獨立發展。
從事件流的角度看,Notice
收到來自事件總線的語意事件後,只負責將其轉化為標準 payload,然後交給 DisplayDriver
,完全不關心它最終是怎麼被送出去的。
在宏觀的城市代理系統中,AnnouncerAgent
(公告代理) 甚至可以動態地去 Directory Facilitator
(黃頁) 查詢當下可用的顯示驅動代理,實現了更高層次的動態組裝。
Andy 恍然大悟,立刻按照總設計師的藍圖重構了他的程式碼。你看,現在的結構多麼清晰!
from typing import Protocol, Dict, Any
# 註:Python < 3.8 可改從 typing_extensions 匯入 Protocol。
# Implementor:設備/通道驅動的共同介面(可替換、可擴張)
# 這就是我們說的「怎麼說」
class DisplayDriver(Protocol):
def send(self, payload: Dict[str, Any]) -> None: ...
# 每個 Driver 只專心做好一件事:跟特定設備溝通
class LedVendorADriver:
def send(self, payload: Dict[str, Any]) -> None:
# 這裡封裝了與 LED Vendor A SDK 溝通的複雜細節
print(f"[LED Vendor A] Sending: {payload}")
class AppPushDriver:
def send(self, payload: Dict[str, Any]) -> None:
# 這裡負責呼叫行動推播服務
print(f"[App Push] Sending: {payload}")
# Abstraction:公告抽象,專注於「要說什麼」
class Notice:
def __init__(self, driver: DisplayDriver):
self._driver = driver
def set_driver(self, driver: DisplayDriver):
self._driver = driver
# 子類別負責決定公告的具體內容與樣式
def render(self) -> Dict[str, Any]:
raise NotImplementedError
# publish 方法將 render 的結果交給 driver
def publish(self):
payload = self.render()
print(f"[Notice] Rendering payload: {payload}")
self._driver.send(payload)
# RefinedAbstraction:具體的公告類型
class TrafficAlert(Notice):
def __init__(self, driver: DisplayDriver, msg: str, locale: str = "zh"):
super().__init__(driver)
self.msg, self.locale = msg, locale
def render(self) -> Dict[str, Any]:
title = "交通速報" if self.locale == "zh" else "Traffic Alert"
return {"title": title, "body": self.msg, "style": "warning"}
# --- 使用場景(完全符合驗收標準!)---
print("--- SCENARIO START ---")
# Given:一份「交通速報」與既有的 LED 顯示板
led_driver = LedVendorADriver()
alert = TrafficAlert(led_driver, "X1 路口壅塞")
alert.publish()
print("\n--- SWITCHING DRIVER ---")
# When:採購換了供應商,再加「App 推播」
app_driver = AppPushDriver()
alert.set_driver(app_driver)
# Then:完全不用修改 TrafficAlert 類別,公告照常送達!
alert.publish()
print("\n--- SCENARIO END ---")
關鍵差異 ✨
反例把語意與通道/供應商混為一談;正解則用 Notice
(抽象)與 DisplayDriver
(實作)漂亮地分離了兩個維度。
現在,新增設備/供應商 = 只需要增加一個 Driver 類別;新增公告類型 = 只需要增加一個 Notice 的子類別。
測試範圍縮小:我們可以分別獨立地測試 render()
的輸出是否正確,以及每個 Driver
的通訊行為是否正常。
總設計師離開前,笑著留下了幾個問題,考驗一下你是否真的掌握了 Bridge 的精髓:
動手實作:請你實作一個 WebTickerDriver
,它會把公告送到官網的跑馬燈上。挑戰是在完全不修改任何 Notice
或 TrafficAlert
類別的前提下完成。
開放問題:回頭看看 3. 笑中帶淚
中 Andy 的那個失敗場景。如果你是當時的英雄,你會如何使用 Bridge 模式重構 NoticeService
類別,讓它既能處理不同公告,又能彈性地切換輸出設備?
反模式紅旗 🚩:在專案中,你聞到了哪些「味道」,會讓你警覺可能有人誤用了 Bridge 模式?
抽象洩漏:Notice
的 render()
方法裡,還看得到 if device == 'LED'
這樣的判斷。
偽 Bridge:有人建立了一個萬能的 GodDriver
,裡面用 if/else
塞進了所有設備的處理邏輯。
維度搞錯:團隊其實只是需要切換演算法(Strategy),卻大費周章地建了一座橋,導致結構過於複雜。
將今天的 Bridge 模式放到更宏觀的架構視野中,你會發現它與許多重要概念遙相呼應:
六角形架構 (Hexagonal):Notice
就是應用程式核心定義的 Port;而每一個 Driver
都是一個具體的 Adapter。更換供應商,就等於是插拔不同的 Adapter,核心領域邏輯紋絲不動。
企業整合模式 (EIP):Notice
負責定義訊息的語意與標準的 payload;而 Driver
則決定了訊息要走哪個 Outbound Channel(例如 WebSocket、HTTP POST 或 MQTT)。
多代理系統 (MAS):「公告代理」可以根據情境,動態地從「黃頁 (DF)」查詢可用的「設備驅動代理」,並透過標準的「代理通訊語言 (ACL)」將任務派發出去,實現了運行時的動態組裝。
Given:一份「交通速報」與既有的 LED 顯示板(LedVendorADriver
)。
When:市府更換供應商,並追加「App 推播」(AppPushDriver
)。
Then:不修改任何公告類別 (TrafficAlert
),僅透過 set_driver()
替換/新增驅動程式,所有公告皆能依情境示範(見上方 Scenario)正常送達,驗收通過。
契約測試:確保所有 DisplayDriver
的實作都遵循 send(payload)
接受 {title, body}
最小集合的契約(見下方的極簡契約測試)。
替換測試:以假的 TestDriver
注入 Notice
,驗證 publish()
僅呼叫一次 send()
,且 payload 內容正確。
端到端測試:模擬 IncidentBus
發出各類事件,驗證 Notice
能正確 render()
並觸發對應的 Driver
送出訊息,同時觀察 ack/err 的處理。
下面這段程式碼強化了「抽象可測、實作可替換」的核心概念,而且不需要引入任何測試框架。
class TestDriver:
def __init__(self): self.sent=[]
def send(self, payload): self.sent.append(payload)
# 注入假的 TestDriver 來驗證抽象層的行為
td = TestDriver()
TrafficAlert(td, "X1 路口壅塞").publish()
assert td.sent and td.sent[0]["title"] in ("交通速報", "Traffic Alert")
print("\n✅ Contract Test Passed: Notice correctly called driver's send method.")
今日總結:抽象與實作分離;兩軸獨立演進,避免類別組合爆炸。
明日預告:今天的公告只能單獨發送,但如果我想把多則公告「打包」成一個群組,像操作單一公告一樣統一顯示呢?敬請期待 Composite (組合模式) 的登場!
為了確保在不支援 Mermaid 渲染的環境中也能正常閱讀,以下提供文中圖表的 ASCII 替代版本:
<<Abstraction>> <<Implementor>>
╔════════════════════╗ ╔═════════════════════╗
║ Notice ║ bridges║ DisplayDriver ║
║────────────────────║───────▶║ <<interface>> ║
║ + publish() ║ ║─────────────────────║
║ + setDriver() ║ ║ + send(payload) ║
╚════════════════════╝ ╚═════════════════════╝
△ △
| | implements
╔══════════════╗ ╔════════════════════╗
║ TrafficAlert ║ ║ LedVendorADriver ║
╚══════════════╝ ╚════════════════════╝
╔══════════════╗ ╔════════════════════╗
║ EventPromo ║ ║ AppPushDriver ║
╚══════════════╝ ╚════════════════════╝
🚌 IncidentBus 🎨 Notice Abstraction 📡 DisplayDriver Implementor
│ │ │
│ on(Event{...}) │ │
│─────────────────────▶│ │
│ │ send({title, ...}) │
│ │─────────────────────▶│
│ │ │
│ │ ack/err │
│ │◀─────────────────────│
│ │ │
📢 Announcer Agent 📖 Directory Facilitator 🔌 Driver Agents
│ │ │
│ lookup("DisplayDriver")│ │
│────────────────────────▶│ │
│ │ │
│ endpoint(s) │ │
│◀────────────────────────│ │
│ │ │
│ send(payload) │ │
│───────────────────────────────────────────────▶│
│ │ │
│ ack │ │
│◀───────────────────────────────────────────────│
│ │ │