程式碼設計
與使用者溝通使用繁體中文。下列術語第一次出現時中英並列,之後依本表統一用法。
設計 deep module:在小介面背後放大量行為,放在乾淨的 seam 上,並且能透過那個介面測試。凡是在設計或重構程式碼的地方,都使用這套語言和這些原則。目標是:給呼叫端槓桿(leverage)、給維護者局部性(locality)、讓所有人都容易測試。
術語表
精確使用以下術語:不要換成「元件」「服務」「API」或「邊界」。一致的語言正是重點所在。
模組(Module):任何有介面和實作的東西。刻意不限規模:可以是一個函式、類別、套件,或跨越多層的切片。避免使用:單元、元件、服務。
介面(Interface):呼叫端要正確使用這個模組所必須知道的一切:型別簽章,以及不變條件、呼叫順序限制、錯誤模式、必要設定、效能特性。避免使用:API、簽章(太窄,只指型別層面的表面)。
實作(Implementation):模組內部的東西,也就是它的程式碼本體。和**轉接器(Adapter)**不同:一個東西可以是有大量實作的小轉接器(例如一個 SQLite repository),也可以是只有少量實作的大轉接器(例如一個記憶體內的假物件)。討論 seam 時用「轉接器」,其他時候用「實作」。
深度(Depth):介面上的槓桿。呼叫端(或測試)每學一單位介面,能使用到多少行為。大量行為藏在小介面背後的模組是深的;介面幾乎和實作一樣複雜的模組是淺的。
Seam(Michael Feathers):一個不必修改該處就能改變行為的地方;也就是模組介面所在的位置。seam 放在哪裡本身就是一個設計決策,和 seam 背後放什麼是兩回事。避免使用:邊界(和 DDD 的 bounded context 混淆)。
轉接器(Adapter):在某個 seam 上滿足某個介面的具體東西。描述的是角色(它填哪個位置),而不是內容(裡面有什麼)。
槓桿(Leverage):呼叫端從深度得到的好處。每學一單位介面,能得到更多能力。一份實作的價值,會在 N 個呼叫處和 M 個測試中回收。
局部性(Locality):維護者從深度得到的好處。修改、bug、知識、驗證都集中在一個地方,而不是散落在各個呼叫端。修一次,處處生效。
深與淺
Deep module = 小介面+大量實作:
┌─────────────────────┐
│ 小介面 │ ← 方法少、參數簡單
├─────────────────────┤
│ │
│ 深層實作 │ ← 複雜邏輯藏在裡面
│ │
└─────────────────────┘
淺模組 = 大介面+少量實作(避免):
┌─────────────────────────────────┐
│ 大介面 │ ← 方法多、參數複雜
├─────────────────────────────────┤
│ 薄實作 │ ← 只是把東西傳過去
└─────────────────────────────────┘
設計介面時,問自己:
- 能減少方法的數量嗎?
- 能簡化參數嗎?
- 能把更多複雜度藏進裡面嗎?
原則
- 深度是介面的性質,不是實作的性質。 一個 deep module 內部可以由小的、可 mock、可替換的零件組成;只是這些零件不屬於介面。模組可以有內部 seam(實作私有、供自己的測試使用),也有位於介面上的外部 seam。
- 刪除測試。 想像把這個模組刪掉。如果複雜度消失了,它只是個傳話筒。如果複雜度在 N 個呼叫端重新出現,它就是有在發揮作用。
- 介面就是測試面。 呼叫端和測試跨越的是同一個 seam。如果你想要越過介面去測試,這個模組的形狀很可能是錯的。
- 一個轉接器代表假想的 seam,兩個轉接器才是真的 seam。 除非真的有東西會在 seam 兩側變化,否則不要引入 seam。
為可測試性而設計
好的介面讓測試變得自然:
-
接收依賴,而不是自己建立依賴。
# 可測試 def fetch_alternates(mpn: str, client: PartSearchClient) -> list[Part]: ... # 難以測試 def fetch_alternates(mpn: str) -> list[Part]: client = DistributorClient(api_key=os.environ["DIST_API_KEY"]) -
回傳結果,而不是產生副作用。
# 可測試 def landed_cost(bom: Bom, freight: Freight) -> Money: ... # 難以測試 def apply_landed_cost(bom: Bom) -> None: bom.total += freight_charge -
小的表面積。 方法越少,需要的測試越少;參數越少,測試準備越簡單。
關係
- 一個模組恰好有一個介面(它呈現給呼叫端和測試的表面)。
- 深度是模組的性質,以它的介面來衡量。
- Seam 是模組的介面所在之處。
- 轉接器位於 seam 上,並滿足介面。
- 深度為呼叫端產生槓桿,為維護者產生局部性。
不採用的框架
- 把深度定義為實作行數與介面行數的比例(Ousterhout):這會獎勵灌水的實作。我們改用「深度即槓桿」。
- 把「介面」等同於 Python 的
Protocol/抽象類別,或類別的公開方法:太窄了:這裡的介面包含呼叫端必須知道的每一件事。 - 「邊界」:和 DDD 的 bounded context 混淆。請說 seam 或介面。
進一步閱讀
- 在既有依賴下加深一群模組,見 DEEPENING.md:依賴分類、seam 紀律,以及「取代而非疊加」的測試策略。
- 探索不同的介面設計,見 DESIGN-IT-TWICE.md:平行派出多個子 agent,用幾種截然不同的方式設計介面,再從深度、局部性與 seam 位置比較。