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,可以依序問四個問題:

  1. 夠深嗎? 介面小、實作複雜嗎?如果介面太大,縮小介面並把複雜度吞進模組。
  2. Seam 切對了嗎? 是否有兩個以上的 Adapter?若只有一個實作,考慮拿掉 Fake Seam。
  3. 依賴分類清楚嗎? 對照四種 Dependency Category,決定 Seam 與測試策略。
  4. 介面只想了一版嗎? 使用 Design It Twice,至少提出三個版本再比較。

參考資料:

  • John Ousterhout,《A Philosophy of Software Design》
  • Michael Feathers,《Working Effectively with Legacy Code》