🎯 什麼情境該想到我

當你「有兩個會各自變化的維度,而用繼承會產生 個類別」的時候——
「訊息類型(一般/緊急)× 發送管道(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/slogslog.Logger 的 API★ slog.Handler(Text、JSON、自訂)
跨平台 GUI元件邏輯各平台的繪圖 API

★ slog 是很好的例子:Logger 那側可以加 With、WithGroup 等新能力,
Handler 那側可以換輸出格式,兩邊完全獨立演化。

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • ★ 只有一個維度會變就不要用。那是 策略模式,不需要抽象階層。
  • ★ 不要預先假設「以後會有第二個維度」。YAGNI ——
    等真的出現第二個維度(你已經在寫第 4 個交叉類別)再重構成 Bridge。
    過早引入橋接會讓簡單的東西變成兩層間接。
  • 維度必須真的獨立。如果「緊急訊息只能走 SMS」,那兩個維度就不正交,強行橋接會產生無效組合。
    → 無效組合要在建構時擋掉(工廠方法模式 或建構時驗證)。
  • 多一層間接 → 除錯與追蹤變難。堆疊裡多一跳,效能上多一次動態分派。
  • GoF 的用語很容易誤導:「Implementor」不是「實作 Abstraction 的類別」,
    而是另一個獨立的介面階層。面試時把這點講清楚會加分。
  • 三個以上的維度:Bridge 只處理兩個維度。三個維度要嵌套或改用其他手法(如參數化設定)。

🔗 相關工具