模組化與依賴方向#
架構風格決定了系統的宏觀骨架,但骨架的好壞,最終取決於微觀的模組化品質:模組內部是否內聚、模組之間如何依賴、依賴指向哪個方向。本章是 4R 模型中 Role(角色切分) 與 Relation(角色關係) 的方法論延伸,討論一套不分架構風格都通用的判準。
模組化的黃金法則只有一句:高內聚、低耦合。本章把這句口號拆成可操作的量尺——內聚的七個層級、耦合的方向、共生性,以及讓依賴方向「永遠指向更穩定處」的規則。
內聚的七個層級#
內聚(Cohesion)衡量「一個模組內的元素是否屬於同一件事」。由最差到最好分七層,是評估模組職責是否單純的細緻量尺。
| 等級 | 內聚類型 | 模組內元素的關係 | 好壞 |
|---|---|---|---|
| 1 | 偶然內聚(Coincidental) | 毫無關係,硬湊在一起 | 最差 |
| 2 | 邏輯內聚(Logical) | 同一類「邏輯」,用旗標選分支 | 差 |
| 3 | 時間內聚(Temporal) | 只因「同時執行」而放一起 | 差 |
| 4 | 程序內聚(Procedural) | 依固定順序執行 | 普通 |
| 5 | 通訊內聚(Communicational) | 操作同一份資料 | 好 |
| 6 | 順序內聚(Sequential) | 前者輸出是後者輸入 | 較好 |
| 7 | 功能內聚(Functional) | 所有元素恰好完成單一明確任務 | 最佳 |
不必追求每個模組都達到「功能內聚」,但若發現一個模組落在第 1 ~ 2 層(偶然/邏輯內聚),就是強烈的拆分訊號。常見的
Utils、Helper、Manager大雜燴多半是偶然內聚。
耦合的兩個方向#
耦合(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 同源:源碼依賴只能由外向內,靠端口-適配器反轉 |
| 循環依賴 | 頭號反模式,發現即斷環 |
自問三題評估模組化:這個模組「只做一件事」嗎(內聚)?它依賴的東西比它更穩定嗎(方向)?有沒有任何環(循環)?三題全過,模組化大致健康。