🎯 什麼情境該想到我
當你「有兩個會各自變化的維度,而用繼承會產生 個類別」的時候——
「訊息類型(一般/緊急)× 發送管道(Email/SMS/Slack)」「形狀 × 繪圖後端」「UI 元件 × 主題」。
⚙️ 怎麼用
意圖
把抽象與實作分離,使它們都能獨立地變化。
⚠️ GoF 的這句話是二十三個模式裡最難懂的。 白話版:
「當你有兩個獨立變化的維度時,不要用繼承把它們交叉相乘,改用組合把它們接起來。」
★ 問題:類別爆炸
❌ 用繼承:m × n 個類別
Message
┌────────────┴────────────┐
NormalMessage UrgentMessage
┌────┼────┐ ┌────┼────┐
Email SMS Slack Email SMS Slack
★ 6 個類別。加一個管道 → 再加 2 個;加一個訊息類型 → 再加 3 個
✅ 用橋接:m + n 個
Message(抽象)──持有──▶ Sender(實作介面)
├ NormalMessage ├ EmailSender
└ UrgentMessage ├ SMSSender
└ SlackSender
★ 2 + 3 = 5 個,而且加任一邊都只加 1 個
★ Go 的長相
// ── 實作維度(Implementor)──
type Sender interface {
Send(ctx context.Context, to, subject, body string) error
}
type emailSender struct{ smtp *smtp.Client }
func (e emailSender) Send(ctx context.Context, to, subj, body string) error { ... }
type slackSender struct{ webhook string }
func (s slackSender) Send(ctx context.Context, to, subj, body string) error { ... }
// ── 抽象維度(Abstraction)──
type Message struct {
sender Sender // ★ 這就是「橋」
to string
}
func (m Message) Send(ctx context.Context, subj, body string) error {
return m.sender.Send(ctx, m.to, subj, body)
}
// 精化的抽象(Refined Abstraction)
type UrgentMessage struct{ Message } // ★ Go 的嵌入
func (m UrgentMessage) Send(ctx context.Context, subj, body string) error {
// 多做的事屬於「抽象維度」,和用哪個 sender 無關
return m.Message.Send(ctx, "[緊急] "+subj, body+"\n\n請於 1 小時內回覆。")
}
// 兩個維度自由組合
UrgentMessage{Message{sender: slackSender{hook}, to: "#ops"}}.Send(ctx, ...)★ Go 的 struct 嵌入(見 工具-Go嵌入機制)讓「精化的抽象」很好寫——
它提供了繼承式的語法便利,但底層是組合。
★ Bridge vs Strategy(最常被問的區分)
結構上兩者幾乎一樣(都是「持有一個介面,把工作委派出去」)。差別在意圖與時機:
| Bridge | 策略模式 | |
|---|---|---|
| 解決什麼 | 兩個維度的類別爆炸 | 一個演算法有多種做法 |
| 何時決定 | ★ 通常在建構時決定,之後不換 | ★ 常在執行期切換 |
| 抽象那一側 | 有自己的階層(Normal/Urgent) | Context 通常只有一個 |
| 時機 | 設計之初(預見兩個維度都會變) | 事後(發現條件分支太多) |
★ 一句話:Bridge 是「結構性的事前設計」,Strategy 是「行為性的事後抽換」。
如果你的「抽象」那一側沒有自己的階層(只有一個類別),那它就是 Strategy 不是 Bridge。
TypeScript 的長相
type Sender = (to: string, subject: string, body: string) => Promise<void>;
const emailSender: Sender = async (to, s, b) => { ... };
const slackSender: Sender = async (to, s, b) => { ... };
// 抽象維度用高階函式表達
const urgent = (send: Sender): Sender =>
(to, s, b) => send(to, `[緊急] ${s}`, `${b}\n\n請於 1 小時內回覆。`);
await urgent(slackSender)("#ops", "DB 連線異常", "...");★ 在 TS 裡兩個維度都可以是函式,橋接就變成「函式組合」——
連介面都不用定義。這時它和 裝飾者模式 看起來已經一樣了。
真實案例
| 案例 | 抽象維度 | 實作維度 |
|---|---|---|
database/sql | *sql.DB / *sql.Tx 的 API | ★ driver.Driver(pq、mysql、sqlite) |
log/slog | slog.Logger 的 API | ★ slog.Handler(Text、JSON、自訂) |
| 跨平台 GUI | 元件邏輯 | 各平台的繪圖 API |
★ slog 是很好的例子:Logger 那側可以加 With、WithGroup 等新能力,
Handler 那側可以換輸出格式,兩邊完全獨立演化。
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
- ★ 只有一個維度會變就不要用。那是 策略模式,不需要抽象階層。
- ★ 不要預先假設「以後會有第二個維度」。YAGNI ——
等真的出現第二個維度(你已經在寫第 4 個交叉類別)再重構成 Bridge。
過早引入橋接會讓簡單的東西變成兩層間接。 - 維度必須真的獨立。如果「緊急訊息只能走 SMS」,那兩個維度就不正交,強行橋接會產生無效組合。
→ 無效組合要在建構時擋掉(工廠方法模式 或建構時驗證)。 - 多一層間接 → 除錯與追蹤變難。堆疊裡多一跳,效能上多一次動態分派。
- GoF 的用語很容易誤導:「Implementor」不是「實作 Abstraction 的類別」,
而是另一個獨立的介面階層。面試時把這點講清楚會加分。 - 三個以上的維度:Bridge 只處理兩個維度。三個維度要嵌套或改用其他手法(如參數化設定)。