程式碼設計
設計深度模組:在一個簡潔的介面背後隱藏大量行為,放置在乾淨的接縫處,並透過該介面進行測試。無論在設計或重構程式碼時,都應使用此語言和這些原則。目標是為呼叫方提供槓桿效應,為維護者提供局部性,並為所有人提供可測試性。
詞彙表
請準確使用這些術語:不要替換為「元件」、「服務」、「API」或「邊界」。一致的語言是重點。
模組 (Module):任何擁有介面和實作的東西。刻意不限規模:可以是函式、類別、套件或跨層級的切片。避免使用:單元、元件、服務。
介面 (Interface):呼叫方必須知道的所有資訊,才能正確使用模組:包括型別簽名,但也包含不變量、順序約束、錯誤模式、所需配置和效能特性。避免使用:API、簽名(這兩者太狹隘,僅指型別層級的表面)。
實作 (Implementation):模組內部的東西,其程式碼主體。與轉接器 (Adapter) 不同:一個東西可以是一個小型轉接器但擁有大型實作(例如 Postgres 儲存庫),也可以是一個大型轉接器但擁有小型實作(例如記憶體中的假物件)。當討論重點是接縫時,使用「轉接器」;否則使用「實作」。
深度 (Depth):介面所提供的槓桿效應。呼叫方(或測試)每學習一個單位的介面,所能運用的行為量。當大量行為隱藏在一個小型介面背後時,該模組就是深度的;當介面幾乎與實作一樣複雜時,就是淺薄的。
接縫 (Seam) (Michael Feathers):可以在不修改該處程式碼的情況下改變行為的地方;模組介面所在的位置。將接縫放在哪裡本身就是一個設計決策,與接縫背後放什麼是分開的。避免使用:邊界 (boundary)(與 DDD 的限界上下文 (bounded context) 過度重疊)。
轉接器 (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:啟動多個並行子代理以多種截然不同的方式設計介面,然後根據深度、局部性和接縫位置進行比較。