Codebase Design 核心觀念
從深模組、Seam、Adapter 與 Dependency Category 理解可維護的程式碼設計。
四個核心詞彙
| 詞彙 | 一句話定義 |
|---|---|
| Depth | 介面越小、背後行為越多 = 越深 |
| Seam | 可以替換行為、但不用改那個位置的程式碼的地方 |
| Adapter | 塞進 Seam 的具體實作 |
| Dependency Category | 依賴的種類,決定要不要切 Seam、怎麼測試 |
flowchart LR
subgraph Module
I["Interface\n呼叫者看到的全部"]
Impl["Implementation\n藏在裡面的複雜邏輯"]
I --> Impl
end
Caller["呼叫者"] -->|"只認識 Interface"| I
Impl -.->|"呼叫者不知道\n也不需要知道"| Impl
style I fill:#4CAF50,color:#fff,stroke:none
style Impl fill:#37474F,color:#fff,stroke:none
style Caller fill:#1976D2,color:#fff,stroke:none
1. Depth:模組設計的核心度量
呼叫者用多少介面,能驅動多少行為?
flowchart TB
subgraph deep["Deep Module ✅"]
direction TB
di["2 個方法"]
dd["幾百行複雜邏輯\n全部藏在裡面"]
di --- dd
end
subgraph shallow["Shallow Module ❌"]
direction TB
si["15 個方法\n複雜參數"]
sd["薄薄一層\n只是轉發"]
si --- sd
end
style di fill:#4CAF50,color:#fff,stroke:none
style dd fill:#37474F,color:#fff,stroke:none,font-size:12px
style si fill:#E53935,color:#fff,stroke:none
style sd fill:#FFCDD2,color:#333,stroke:none,font-size:12px
深模組帶來兩個好處:
- Leverage(槓桿):N 個呼叫端從同一個小介面受益。
- Locality(局部性):改一處就修好,不擴散到呼叫端。
刪除測試:把模組刪掉,複雜度是消失了還是散回 N 個呼叫端?散回去,代表模組確實封裝了價值;複雜度完全消失,則可能只是 pass-through 的多餘包裝。
2. Seam:可以替換行為的位置
Seam 是「插槽」,不是獨立的東西;它是 Interface 所在的位置。
flowchart TB
Service["OrderService\n只認識 Interface"]
IF{{"PaymentGateway\ncharge(amount)"}}
Service -->|"依賴注入"| IF
IF -->|"正式環境"| A1["StripeAdapter"]
IF -->|"台灣客戶"| A2["ECPayAdapter"]
IF -->|"測試"| A3["FakeAdapter"]
style Service fill:#1976D2,color:#fff,stroke:none
style IF fill:#FF9800,color:#fff,stroke:none
style A1 fill:#4CAF50,color:#fff,stroke:none
style A2 fill:#4CAF50,color:#fff,stroke:none
style A3 fill:#78909C,color:#fff,stroke:none
判斷 Seam 是否真實存在:
| 情況 | 判定 |
|---|---|
| 有 2 個以上 Adapter(例如 production + test) | 真 Seam ✅ |
| 只有 1 個實作,只是「以防未來」才切 | Fake Seam ❌ |
| 內部 Seam 暴露到外部介面 | 設計錯誤 ❌ |
3. Adapter:填入 Seam 的具體實作
Interface 定義規格、Seam 提供插槽、Adapter 是符合規格的具體實作。
Adapter 描述的是角色(它填了哪個 Seam),不是內容(裡面有多少程式碼)。一個 Adapter 可以是 3 行的 in-memory fake,也可以是 200 行的第三方服務串接。
flowchart LR
subgraph relation["三者關係"]
direction LR
Interface["Interface\n定義規格"] --> Seam["Seam\n提供插槽"] --> Adapter["Adapter\n具體實作"]
end
style Interface fill:#FF9800,color:#fff,stroke:none
style Seam fill:#7B1FA2,color:#fff,stroke:none
style Adapter fill:#4CAF50,color:#fff,stroke:none
4. Dependency Category:決定 Seam 策略
依賴的種類與可控程度,會決定是否需要切 Seam,以及測試時要使用什麼替身:
| 類型 | 例子 | 建議策略 |
|---|---|---|
| 完全在手上 | 數學計算、排序、格式化 | 不用切 Seam,直接寫進去 |
| 跑在本機,可裝替身 | 資料庫、檔案系統 | Seam 藏在內部,測試用輕量替身 |
| 走網路,但是自家的 | 團隊維護的另一支 API | 切 Seam + 注入,測試塞假的、正式塞真的 |
| 完全不是你的 | Stripe、LINE、Twilio | 一定要切 Seam,隔離不可控依賴 |
關鍵原則:對依賴的控制力越低,就越需要切 Seam 來隔離它。
設計決策流程
拿到任何一段 code,可以依序問四個問題:
- 夠深嗎? 介面小、實作複雜嗎?如果介面太大,縮小介面並把複雜度吞進模組。
- Seam 切對了嗎? 是否有兩個以上的 Adapter?若只有一個實作,考慮拿掉 Fake Seam。
- 依賴分類清楚嗎? 對照四種 Dependency Category,決定 Seam 與測試策略。
- 介面只想了一版嗎? 使用 Design It Twice,至少提出三個版本再比較。
參考資料:
- John Ousterhout,《A Philosophy of Software Design》
- Michael Feathers,《Working Effectively with Legacy Code》