🎯 什麼情境該想到我

當你發現「每次加一個新類型,都要回去改同一個 switch/if 鏈」的時候。
★ 但這條原則最常被誤用成「什麼都預先抽象」——所以本卡有一半在講什麼時候不該套用。

⚙️ 怎麼用

原則

對擴充開放,對修改關閉:新增行為靠「加新程式」,而不是「改舊程式」。

為什麼重要:改舊程式碼會重新引入風險(要重測、可能破壞既有行為);加新程式碼不動既有路徑。

三種實現手法(由輕到重)

① 資料驅動(最輕,常被忽略)

// ❌ 加一種幣別就要改函式
func Symbol(cur string) string {
    switch cur {
    case "TWD": return "NT$"
    case "USD": return "$"
    }
}
 
// ✅ 改成查表 —— 加幣別只是加一筆資料
var symbols = map[string]string{"TWD": "NT$", "USD": "$"}

★ 很多「需要開閉原則」的場景,其實只是需要一張表。 先試這個,不要急著抽介面。

② 策略/介面(有行為差異時)

type PaymentMethod interface {
    Charge(ctx context.Context, amt Money) (*Receipt, error)
}
 
// 結帳流程只認介面,加一種付款方式不動它
func Checkout(ctx context.Context, m PaymentMethod, amt Money) error { ... }

→ 見 策略模式、工具-針對介面編程

③ ★ 註冊表(真正做到「加檔案就好」)

var registry = map[string]func(Config) (PaymentMethod, error){}
func Register(kind string, f func(Config) (PaymentMethod, error)) { registry[kind] = f }
 
// ── 新檔案 linepay.go,完全不動既有程式 ──
func init() { Register("linepay", newLinePay) }

★ 這是 database/sql 的做法(sql.Register + sql.Open),也是 Go 生態最常見的插件機制。
→ 見 工廠方法模式

★ 判準:什麼會變?

開閉原則不可能對「所有可能的變化」都成立。
Bertrand Meyer 的原意是:針對「你預期會變的那一個軸」設計擴充點,其他軸保持簡單。

付款方式會變 → 抽 PaymentMethod 介面        ✅
但「結帳流程的步驟順序」不會變 → 寫死就好    ✅

猜錯軸的代價:你為「付款方式」做了漂亮的擴充點,結果變的是「結帳流程」——
抽象不但沒幫上忙,還讓改動更難。

🧪 我實際套用的紀錄

  • 2026-07-14:(待填)

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

  • ★ 最常見的誤用:預先抽象。
    「以後可能會有第二種」不是理由。等第二種真的出現再抽——
    那時你才知道兩者的共同點在哪,抽象的邊界才會正確。
    → 第一次寫死、第二次容忍重複、第三次才抽象(Rule of Three)。
  • ★ 錯誤的抽象比重複更貴(見 工具-DRY原則)。
    抽錯的介面會讓每個新實作都要「塞進不合身的形狀」,最後每個實作都有一半方法回 ErrNotSupported。
  • ★ 開閉原則對「加型別」開放,但對「加操作」封閉——這是它的對偶代價。
    用 PaymentMethod 介面後,加一個新方法要改所有實作。
    這就是表達式問題(見 訪問者模式):你只能選一個軸。
  • Go 的 switch 不一定要消除。如果分支是閉集合且穩定(星期幾、HTTP 方法),
    switch 比介面更清楚也更快。抽介面要有理由,不是反射動作。
  • 註冊表的代價:init() 有副作用、初始化順序不可控、_ 空白匯入讓依賴變隱晦。
    中小型專案直接用 switch 通常更好維護。
  • 這條原則不能單獨用。它的前提是 工具-針對介面編程 與 工具-優先組合而非繼承。

🔗 相關工具