軟體實體應該對擴展開放、對修改封閉:加新行為時,不必去改動既有源碼。由 Bertrand Meyer 提出,靠抽象化解這看似矛盾的兩個目標——模組依賴一個固定的抽象(對修改封閉),新行為以新的實作類別加入(對擴展開放)。
🧠 When to Use
TIP
訊號:每次「加一種類型」都要回去改同一個 when(type) / if...else if 鏈;一個小需求牽動一片模組;本該只是「加東西」的工作變成「到處改」。
- 一個行為會依「型別 / 種類」分歧,而種類還會持續增加。
- 想讓高階流程對「新增變體」保持封閉,把變化點鎖在抽象背後。
- 已經被同一類變更「騙過一次」,決定重構以保護未來同型的變更。
⚖️ Structure & Variants
違反 vs 遵循 — DrawAllShapes
// ❌ 每加一種圖形就要改這個函式 —— 對修改不封閉
fun drawAll(shapes: List<Any>) {
for (s in shapes) when (s) {
is Square -> drawSquare(s)
is Circle -> drawCircle(s)
// 加 Triangle?回來再補一條 else if...
}
}
// ✅ 依賴抽象,新增圖形完全不動 drawAll
interface Shape { fun draw() }
class Square : Shape { override fun draw() { /* ... */ } }
class Circle : Shape { override fun draw() { /* ... */ } }
fun drawAll(shapes: List<Shape>) = shapes.forEach { it.draw() }
// 新增 Triangle:只寫一個新 class 實作 Shape,drawAll 一行都不改- 這正是 Strategy 與 Template Method 兩個模式達成 OCP 的方式:把可變步驟宣告成抽象,客戶端同時 open 又 closed。
沒有『對所有變更都封閉』這回事 — 資料驅動封閉
- 對「新增圖形種類」封閉的設計,遇到「圓形要畫在正方形之前」這種排序需求,就又違反 OCP 了。沒有任何結構對所有變更方向都天然。
- 解法之一是把易變的規則抽到外部資料表(Data-Driven):
// 繪製順序抽成一張優先權表,新增種類只在表裡加一行
val priority = mapOf(Circle::class to 1, Square::class to 2)
val ordered = shapes.sortedBy { priority[it::class] ?: Int.MAX_VALUE }- 各 Shape 與 drawAll 都對「順序變更」保持封閉;變化被關進資料裡。
⚠️ Misuse & Anti-patterns
- 過早抽象:在還沒看到變更模式之前就到處插入抽象層,不但擋不住未來的問題,反而先付出不必要的複雜性。抵抗過早抽象與善用抽象同等重要。
- 想預測所有變更:不可能。書中建議「上當一次(fool me once)」——先寫最簡單的碼,第一次真的被某類變更打到時,才重構以保護「未來同型」的變更。
- 完全封閉的幻想:完全封閉做不到,只能策略性地選擇對哪些變更封閉——靠經驗與對領域的理解下注。
🔑 Takeaways
- OCP 是 OO 設計的核心:擴展靠新增實作,而非修改既有碼。
- 用抽象把「會變的那一維」鎖起來;Strategy / Template Method 是最典型的落地。
- 只對「賭得中的變更軸」封閉,並靠測試、短迭代、持續重構在對的時機引入抽象——別預先鋪滿。
No notes yet — jot your takeaways or Q&A here.