程式碼庫設計
設計深層模組:將大量行為隱藏於小型介面之後,放置在乾淨的接縫處,並可透過該介面進行測試。在設計或重構程式碼時,請使用此語言與這些原則。其目標是為呼叫者提供槓桿效應,為維護者提供局部性,並為所有人提供可測試性。
詞彙表
請精確使用這些術語:不要替換為「元件」、「服務」、「API」或「邊界」。一致的語言是關鍵。
模組 (Module):任何具有介面與實作的事物。刻意地尺度中立:可以是一個函式、類別、套件或跨層級的切片。避免使用:單元、元件、服務。
介面 (Interface):呼叫者為正確使用模組而需知道的一切事物:型別簽名,但也包括不變量、排序約束、錯誤模式、必要設定與效能特性。避免使用:API、簽名(過於狹隘,僅指型別層級的表面)。
實作 (Implementation):模組內部的事物,其程式碼主體。與轉接器 (Adapter) 不同:一個東西可以是一個帶有大型實作的小型轉接器(例如 Postgres 儲存庫),或是一個帶有小型實作的大型轉接器(例如記憶體中的假物件)。當討論焦點是接縫時,請使用「轉接器」;其他情況則使用「實作」。
深度 (Depth):介面上的槓桿效應。呼叫者(或測試)每學習一個介面單位,所能運用的行為量。當大量行為位於小型介面之後時,模組即為深層;當介面幾乎與實作一樣複雜時,模組即為淺層。
接縫 (Seam) (Michael Feathers):一個你可以在不修改該處程式碼的情況下改變行為的位置;也是模組介面所處的位置。接縫要放在哪裡本身就是一個設計決策,與放在它後面是什麼東西是分開的。避免使用:邊界(與 DDD 的限界上下文過度重載)。
轉接器 (Adapter):滿足接縫處介面的具體事物。描述其角色(填補什麼插槽),而非本質(內部是什麼)。
槓桿效應 (Leverage):呼叫者從深度中獲得的效益。每學習一個介面單位,就能獲得更多能力。一個實作在 N 個呼叫點與 M 個測試中回報其價值。
局部性 (Locality):維護者從深度中獲得的效益。變更、錯誤、知識與驗證集中在一個地方,而不是分散到多個呼叫者。修復一處,處處修復。
深度 vs 淺層
深層模組 = 小型介面 + 大量實作:
┌─────────────────────┐
│ 小型介面 │ ← 少量的方法、簡單的參數
├─────────────────────┤
│ │
│ 深層實作 │ ← 隱藏複雜的邏輯
│ │
└─────────────────────┘
淺層模組 = 大型介面 + 少量實作(應避免):
┌─────────────────────────────────┐
│ 大型介面 │ ← 大量的方法、複雜的參數
├─────────────────────────────────┤
│ 薄層實作 │ ← 僅是傳遞
└─────────────────────────────────┘
設計介面時,請問:
- 我能減少方法的數量嗎?
- 我能簡化參數嗎?
- 我能將更多複雜性隱藏在內部嗎?
原則
- 深度是介面的屬性,而非實作的屬性。 一個深層模組內部可以由許多小的、可模擬的、可替換的部件組成;但它們不是介面的一部分。一個模組可以有內部接縫(私有於其實作,供其自身測試使用)以及其介面上的外部接縫。
- 刪除測試。 想像刪除這個模組。如果複雜性消失,它只是一個傳遞者。如果複雜性分散到 N 個呼叫者身上,那麼它就是在發揮其價值。
- 介面即測試面。 呼叫者與測試跨越同一條接縫。如果你想繞過介面進行測試,模組的形狀可能不正確。
- 一個轉接器代表一個假設性的接縫。兩個轉接器代表一個真實的接縫。 除非有事物實際在接縫處變化,否則不要引入接縫。
為可測試性設計
良好的介面能讓測試變得自然:
-
接受依賴,而非建立它們。
// 可測試 function processOrder(order, paymentGateway) {} // 難以測試 function processOrder(order) { const gateway = new StripeGateway(); } -
回傳結果,而非產生副作用。
// 可測試 function calculateDiscount(cart): Discount {} // 難以測試 function applyDiscount(cart): void { cart.total -= discount; } -
小表面積。 方法越少 = 需要的測試越少。參數越少 = 測試設定越簡單。
關係
- 一個模組恰有一個介面(它向呼叫者和測試呈現的表面)。
- 深度是模組的一個屬性,根據其介面來衡量。
- 接縫是模組的介面所處的位置。
- 轉接器位於接縫處,並滿足介面。
- 深度為呼叫者產生槓桿效應,為維護者產生局部性。
被拒絕的框架
- 將深度視為實作行數與介面行數的比率 (Ousterhout):會獎勵填充實作。我們改用深度即槓桿效應。
- 將「介面」視為 TypeScript 的
interface關鍵字或類別的公開方法:過於狹隘:這裡的介面包含呼叫者需要知道的每一項事實。 - 「邊界」:與 DDD 的限界上下文過度重載。請說接縫或介面。
深入探索
- 根據其依賴關係深化一個叢集,請參閱 DEEPENING.md:依賴分類、接縫紀律與取代而非分層的測試。
- 探索替代介面,請參閱 DESIGN-IT-TWICE.md:啟動平行子代理以幾種完全不同的方式設計介面,然後在深度、局部性與接縫位置上進行比較。