🎯 什麼情境該想到我
當你發現「每次加一個新類型,都要回去改同一個 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通常更好維護。 - 這條原則不能單獨用。它的前提是 工具-針對介面編程 與 工具-優先組合而非繼承。