封裝變化 (Encapsulate What Varies)

★★★★★Studied

FromDesign Patterns Explained

📚 From the Books

封裝變化 (Encapsulate What Varies)ConceptPrinciples

找出變動軸並把它隔離:概念上穩定的部分(commonality)收成抽象,底下會變的具體(variability)做成衍生。這是設計模式共同的底層動機——CVA 與設計模式最終得到極相似的結構,正因為兩者遵循同一組原則:找出變動、封裝、設計到介面、聚合勝於繼承。

🧠 When to Use

TIP

訊號:整合新程式進既有系統的成本,比新程式本身的開發成本還高——通常是元件之間耦合過緊。或你發現自己(越有經驗越容易)太早在想「實體之間如何關聯」,卻還沒確認「實體本身對不對」。

⚖️ Structure & Variants

CVA 兩階段 — 先找概念,再從使用方反推介面

共通與差異分析(Commonality and Variability Analysis, CVA)是一套全域可用的找抽象方法,任何問題都適用。設計流程:

  1. 識別 commonality(概念)
  2. 從 commonality 建立抽象
  3. 用 variability(具體變化)建立衍生
  4. 觀察 commonality 之間的關係

找概念的一個技巧:兩兩比對「這是那個的變化嗎?」「這兩者是某概念的不同變化嗎?」找到概念後,每個 commonality 變成抽象類別、其變化變成衍生類別。

// commonality「Feature」→ 抽象;variability(slot/hole/cutout…)→ 衍生
interface Feature {
    fun extractFrom(cadcam: CadCam)
}
class Slot : Feature { override fun extractFrom(cadcam: CadCam) { /* ... */ } }
class Hole : Feature { override fun extractFrom(cadcam: CadCam) { /* ... */ } }
// 加一種 feature 只是多一個衍生類別,抽象與使用方不動

第二階段:從使用方視角反推每個抽象的介面(即依賴倒置原則的應用)。

一個 commonality 只承載一個議題

若把 CAD/CAM 問題拆成 V1SlotsV1HolesV2SlotsV2Holes……,共通性看起來是「CAD/CAM features」,但其實混了兩個議題:CAD/CAM 版本 + feature 種類。一個 commonality 應只承載一個議題,否則設計凝聚會被破壞。

// ❌ 一個維度混了「版本」與「feature 種類」兩個會獨立變動的議題
class V1Slot; class V1Hole; class V2Slot; class V2Hole
// 每加一種 feature × 每多一個版本,類別數相乘爆炸

// ✅ 拆成兩條各自獨立的變動軸
interface Feature                       // 議題一:feature 種類
interface CadCam                        // 議題二:CAD/CAM 版本

這條規則也是作者的個人紀律:從不讓一個類別同時承載兩個會獨立變動的議題,除非這兩者本就緊密耦合。對照 單一職責原則(一個類別只有一個被變更的理由)。

各模式都在封裝某一種變動

「封裝變動」是貫穿各模式的元原則,每個模式各自封裝一種變動軸:

  • Bridge:封裝實作的變動——加新實作完全不用動既有類別(也是 OCP 的典型示範)。
  • Abstract Factory:封裝「該建立哪一群物件」的變動。
  • Adapter:把不同介面統一,配合其他模式的變動需求。
  • Facade:通常不是封裝變動,但同一介面包不同子系統時可順便達成。

模式不只在封裝變動,也定義了物件之間的關係——例如 Bridge 同時定義抽象與實作這兩個變動軸如何關聯。變動找出來之後「用什麼實現」,見 組合優於繼承

抽象類別 vs 介面 — 封裝什麼由此決定
  • 抽象類別:把相關實作收攏(focus on encapsulating implementations),仍可遵循依賴倒置。
  • 介面:思考「使用方需要什麼樣的介面」,可拆得更細、更精簡(thin interface)。

兩者不是一個比另一個好。可先用介面定義出最小契約,再用抽象類別實作以共享預設行為。

interface Feature { fun extractFrom(cadcam: CadCam) }   // 使用方要的最小契約
abstract class AbstractFeature : Feature {              // 收攏共同實作
    protected fun log(msg: String) { /* 衍生類共享 */ }
}

⚠️ Misuse & Anti-patterns

🔑 Takeaways

✍️ My Notes

No notes yet — jot your takeaways or Q&A here.

📖 Further Reading