模組化與依賴方向#

架構風格決定了系統的宏觀骨架,但骨架的好壞,最終取決於微觀的模組化品質:模組內部是否內聚、模組之間如何依賴、依賴指向哪個方向。本章是 4R 模型Role(角色切分)Relation(角色關係) 的方法論延伸,討論一套不分架構風格都通用的判準。

模組化的黃金法則只有一句:高內聚、低耦合。本章把這句口號拆成可操作的量尺——內聚的七個層級、耦合的方向、共生性,以及讓依賴方向「永遠指向更穩定處」的規則。

內聚的七個層級#

內聚(Cohesion)衡量「一個模組內的元素是否屬於同一件事」。由最差到最好分七層,是評估模組職責是否單純的細緻量尺。

等級內聚類型模組內元素的關係好壞
1偶然內聚(Coincidental)毫無關係,硬湊在一起最差
2邏輯內聚(Logical)同一類「邏輯」,用旗標選分支
3時間內聚(Temporal)只因「同時執行」而放一起
4程序內聚(Procedural)依固定順序執行普通
5通訊內聚(Communicational)操作同一份資料
6順序內聚(Sequential)前者輸出是後者輸入較好
7功能內聚(Functional)所有元素恰好完成單一明確任務最佳

不必追求每個模組都達到「功能內聚」,但若發現一個模組落在第 1 ~ 2 層(偶然/邏輯內聚),就是強烈的拆分訊號。常見的 UtilsHelperManager 大雜燴多半是偶然內聚。

耦合的兩個方向#

耦合(Coupling)衡量模組間的相互依賴強度。從一個模組的視角看,耦合有方向:

  • 傳出耦合(Efferent / Outgoing):本模組依賴了多少其他模組(我「需要」別人)。
  • 傳入耦合(Afferent / Incoming):有多少其他模組依賴本模組(別人「需要」我)。

這引出一個衡量「穩定性」的概念。一個被很多人依賴、卻幾乎不依賴別人的模組(高傳入、低傳出),改動成本極高,因此應該保持穩定

$$ I = \frac{C_e}{C_e + C_a} $$

其中 $C_e$ 為傳出耦合數、$C_a$ 為傳入耦合數,$I$ 為不穩定度(Instability),介於 0 ~ 1。

  • $I \to 0$:極穩定(大家依賴它,它不依賴別人)➡️ 適合放抽象、核心領域。
  • $I \to 1$:極不穩定(它依賴一堆別人,沒人依賴它)➡️ 適合放易變的細節、最外層。

設計依賴方向的鐵律:源碼依賴只能指向更穩定($I$ 更小)的方向。讓易變的東西依賴穩定的東西,而不是反過來。這正是後面 Clean / Hexagonal 依賴規則的數學底層。

共生性(Connascence):更精細的耦合語言#

「耦合」太粗。共生性把「兩段程式碼必須一起改才不會壞」的關係,依強度位置分類,給了重構優先順序的依據。

依強度分類(由弱到強)#

共生類型說明例子
名稱共生(Name)雙方須同意「名字」呼叫方與被呼叫方的方法名
型別共生(Type)須同意「型別」參數型別
意義共生(Meaning)須同意某個值的「約定意義」1 代表男、2 代表女這種魔術數字
位置共生(Position)須同意「順序」位置參數的排列順序
演算法共生(Algorithm)雙方須用同一套演算法收發兩端的加解密/雜湊
執行序共生(Execution)須同意「執行順序」必須先 init 才能 use
時機共生(Timing)須同意「時間」競態條件
值共生(Value)多個值須同時保持一致起訖日期必須同步更新
身分共生(Identity)須引用同一個實體兩處須指向同一物件

前四種屬靜態共生(看程式碼就知道),後五種屬動態共生(要執行才知道),動態共生通常更難維護。

弱化與縮短距離兩大策略#

flowchart LR
    Strong[強共生<br/>意義/位置/演算法] -->|弱化 Degrade| Weak[弱共生<br/>名稱/型別]
    Far[跨模組的共生<br/>距離遠] -->|縮短 Minimize| Near[同一模組內的共生<br/>距離近]

    style Strong fill:#ffebee
    style Weak fill:#e8f5e9
    style Far fill:#fff3e0
    style Near fill:#e8f5e9
  • 弱化(強度):把強共生重構為弱共生。例如把「位置參數(位置共生)」改成「具名參數/結構體(名稱共生)」,把「魔術數字(意義共生)」改成「列舉型別(型別共生)」。
  • 縮短距離:強共生若無法消除,就讓它待在同一個模組內。跨模組的強共生最危險——改一邊、忘了另一邊,編譯器幫不了你。

Clean / Hexagonal:依賴規則#

把「源碼依賴指向更穩定處」具體化,就得到兩個常見的同類架構:整潔架構(Clean Architecture) 的同心圓,與 六角架構(Hexagonal Architecture) 的端口-適配器。它們本質相同。

同心圓與依賴規則#

flowchart TD
    subgraph Outer[最外層 · 最不穩定 I→1]
        FW[框架 / DB / Web / 裝置]
    end
    subgraph Adapter[介面適配層]
        AD[Controller / Presenter / Gateway 實作]
    end
    subgraph UseCase[應用層]
        UC[Use Case · 應用邏輯]
    end
    subgraph Entity[最內層 · 最穩定 I→0]
        EN[Entities · 企業核心規則]
    end

    FW --> AD --> UC --> EN

    style Entity fill:#e8f5e9
    style UseCase fill:#e3f2fd
    style Adapter fill:#fff3e0
    style Outer fill:#ffebee

依賴規則:源碼層級的依賴只能由外向內。 內層完全不知道外層的存在。框架、資料庫、UI 都是「細節」,被推到最外圈;企業核心規則(Entities)在最內圈,最穩定、最不該因技術選型而改變。

端口-適配器:依賴反轉如何跨界#

內層(穩定)若需要呼叫外層(不穩定)的能力(例如 Use Case 需要存資料),不能直接依賴外層,否則就違反依賴規則。解法是 依賴反轉(DIP)

  • 內層定義一個端口(Port,介面)
  • 外層提供一個適配器(Adapter,實作)
  • 控制流由內往外,但源碼依賴方向由外往內(外層的適配器 implements 內層的介面)。
正反例:依賴方向(Go)

反例——應用層直接依賴具體 DB,依賴指向了不穩定的細節:

// 反例:usecase 直接 import 並依賴 MySQL 實作
package usecase

import "myapp/infra/mysql" // 指向外層,違反依賴規則

type OrderService struct {
    db *mysql.OrderTable // 與具體技術綁死,無法替換、難測試
}

func (s *OrderService) Place(o Order) error {
    return s.db.InsertWithMySQL(o) // 業務邏輯被 DB 細節污染
}

正例——應用層只依賴自己定義的端口(介面),DB 實作在外層注入:

// 應用層:定義端口(Port),不知道任何 DB 細節
package usecase

type OrderRepository interface { // 內層擁有的介面
    Save(o Order) error
}

type OrderService struct {
    repo OrderRepository // 依賴抽象,I→0,穩定且可測
}

func (s *OrderService) Place(o Order) error {
    return s.repo.Save(o)
}
// 外層:適配器(Adapter)實作內層介面,依賴方向由外指向內
package mysql

import "myapp/usecase"

type OrderRepoMySQL struct{ /* *sql.DB */ }

func (r *OrderRepoMySQL) Save(o usecase.Order) error {
    // 真正的 MySQL 寫入邏輯
    return nil
}

// 編譯期確認:適配器滿足內層端口
var _ usecase.OrderRepository = (*OrderRepoMySQL)(nil)

差別:正例中換掉 MySQL(改用 Postgres、或測試用記憶體假物件)完全不需動 usecase,因為依賴指向穩定的抽象。

循環依賴反模式#

最該避免的結構,是模組之間形成循環依賴(Circular Dependency):A 依賴 B、B 又依賴 A。

flowchart LR
    A[模組 A] --> B[模組 B]
    B --> A

    style A fill:#ffebee
    style B fill:#ffebee

循環依賴的危害:

  • 無法獨立編譯、測試、部署任一模組——它們黏成一坨。
  • 「穩定度」失去意義(雙方互相依賴,誰也不比誰穩定)。
  • 改動會在環內無限傳染,是「大泥球(Big Ball of Mud)」的典型病徵。

一旦偵測到循環依賴,立刻處理。常見解法:抽出共用部分成第三個模組、用依賴反轉打斷其中一個方向、或合併兩個本就該在一起的模組。切勿放任,環會越長越大。

本章小結#

主題核心觀點
內聚七層由偶然到功能;落在第 1 ~ 2 層是拆分訊號
耦合方向區分傳入/傳出,用不穩定度 $I$ 衡量;依賴指向更穩定處
共生性比耦合更細;策略是「弱化強度」與「縮短距離」
依賴規則Clean / Hexagonal 同源:源碼依賴只能由外向內,靠端口-適配器反轉
循環依賴頭號反模式,發現即斷環

自問三題評估模組化:這個模組「只做一件事」嗎(內聚)?它依賴的東西比它更穩定嗎(方向)?有沒有任何環(循環)?三題全過,模組化大致健康。