程式碼庫設計
設計深模組:小介面背後有大量行為、放在乾淨的接縫上、可以透過那個介面測試。在任何程式碼被設計或重構的地方使用這套語言與這些原則。目標是讓呼叫者獲得槓桿收益、維護者獲得局部性、所有人獲得可測試性。
詞彙表
精確使用這些術語——不要替換成「component」「service」「API」或「boundary」。一致的語言就是重點。
模組——任何有介面與實作的東西。刻意地與規模無關:一個函式、類別、套件,或橫跨層級的切片。Avoid: unit、component、service。
介面——呼叫者要正確使用模組所需知道的一切:型別簽名,也包括不變量、順序約束、錯誤模式、必要的設定,與效能特徵。Avoid: API、signature(太窄——它們只指型別層級的表面)。
實作——模組裡面的東西,它的程式碼本體。與轉接器區別:一個東西可以是小轉接器配大實作(Postgres repo),或大轉接器配小實作(記憶體中的假物件)。當主題是接縫時用「轉接器」;其他情況用「實作」。
深度——介面上的槓桿收益:呼叫者(或測試)每學習一單位介面所能行使的行為量。當大量行為藏在一個小介面後面時,模組是深的;當介面幾乎跟實作一樣複雜時是淺的。
接縫 (Michael Feathers)——一個你可以不用在原地編輯就能改變行為的地方;模組介面所在的位置。接縫放哪裡本身是一個設計決策,與放在它後面的是什麼是兩回事。Avoid: boundary(與 DDD 的 bounded context 過載)。
轉接器——在接縫處滿足某個介面的具體東西。描述角色(它填補什麼槽位),不是實體(裡面是什麼)。
槓桿收益——呼叫者從深度得到的:每學習一單位介面獲得更多能力。一份實作在 N 個呼叫點與 M 個測試之間回本。
局部性——維護者從深度得到的:變更、bug、知識與驗證集中在一個地方,而不是散落在呼叫者之間。修一次,處處修好。
深 vs 淺
深模組 = 小介面 + 大量實作:
┌─────────────────────┐
│ Small Interface │ ← Few methods, simple params
├─────────────────────┤
│ │
│ Deep Implementation│ ← Complex logic hidden
│ │
└─────────────────────┘
淺模組 = 大介面 + 少許實作(避免):
┌─────────────────────────────────┐
│ Large Interface │ ← Many methods, complex params
├─────────────────────────────────┤
│ Thin Implementation │ ← Just passes through
└─────────────────────────────────┘
設計介面時問:
- 我能減少方法數量嗎?
- 我能簡化參數嗎?
- 我能把更多複雜度藏在裡面嗎?
原則
- 深度是介面的屬性,不是實作的屬性。 深模組可以在內部由小而可模擬、可替換的部件組成——它們只是不是介面的一部分。模組可以同時有內部接縫(實作私有、供自己的測試使用)以及位於其介面上的外部接縫。
- 刪除測試。 想像刪掉這個模組。如果複雜度消失,它只是個轉送層。如果複雜度在 N 個呼叫者之間重現,它在賺自己的住宿費。
- 介面就是測試表面。 呼叫者和測試跨越同一個接縫。如果你想測試到介面之後,模組大概形狀錯了。
- 一個轉接器意味著假設性接縫;兩個轉接器意味著真實接縫。 除非有什麼東西真的跨越它而變化,否則不要引入接縫。
為可測試性設計
好介面讓測試很自然:
接受相依,不要製造相依。
// Testable function processOrder(order, paymentGateway) {} // Hard to test function processOrder(order) { const gateway = new StripeGateway(); }回傳結果,不要製造副作用。
// Testable function calculateDiscount(cart): Discount {} // Hard to test function applyDiscount(cart): void { cart.total -= discount; }小表面積。 方法更少 = 需要的測試更少。參數更少 = 測試設定更簡單。
關係
- 一個模組剛好有一個介面(它呈現在呼叫者與測試面前的表面)。
- 深度是模組的屬性,相對於它的介面來衡量。
- 接縫是模組的介面所在之處。
- 轉接器坐在接縫處並滿足介面。
- 深度為呼叫者產生槓桿收益、為維護者產生局部性。
被否決的框架
- 把深度當成實作行數對介面行數的比率(Ousterhout):獎勵灌水實作。我們改用深度即槓桿收益。
- 把「介面」當成 TypeScript 的
interface關鍵字或類別的公開方法:太窄——這裡的介面包含呼叫者必須知道的每一件事實。 - 「boundary」:與 DDD 的 bounded context 過載。說seam或interface。
深入下去
- 考量其相依而深化一個叢集——見 DEEPENING.md:相依分類、接縫紀律,以及「取代而不分層」的測試。
- 探索替代介面——見 DESIGN-IT-TWICE.md:並行啟動子代理,用幾種截然不同的方式設計介面,然後在深度、局部性與接縫位置之間比較。