專案越大,修改一個檔案就越令人害怕,因為無法預估影響會擴散到哪裡。
建置速度變慢,程式碼審查範圍變廣。新成員也會苦惱該從哪裡開始閱讀。
模組化是解決這個問題最古老的方法。簡單說,就是把大型程式碼區塊拆成能獨立理解與替換的單位。
本文將整理模組化究竟在拆分什麼、以內聚力和耦合度判斷好模組的標準,以及何時該拆分。
先看重點摘要。
- 模組化是把「一起變更的東西」放在一起,建立邊界
- 好模組的標準是高內聚力、低耦合度
- 目的首先是降低變更成本,而不是提升建置速度
- 在邊界不清楚時拆分,反而只會增加複雜度
模組化究竟在拆分什麼?
拆分資料夾和拆分模組是兩回事。
資料夾只是整理檔案。任何資料夾中的程式碼都能自由使用其他資料夾的程式碼。
模組則會形成具強制力的邊界。在模組外,只能使用該模組公開的介面。
模組化的本質不是整理檔案,而是讓編譯器強制區分「外部需要知道的事」和「不需要知道的事」。
有了這個邊界,就能做到兩件事。
- 獨立理解:不必了解內部,只看介面就能使用
- 獨立替換:介面相同時,即使整個替換內部實作,外部也不受影響
這就是David Parnas在1972年論文中提出的資訊隱藏概念。拆分模組的標準不是功能,而是「想隱藏的設計決策」。
好模組的標準:內聚力與耦合度
拆分模組後反而變得不方便,多半是這兩項指標失衡。
內聚力表示模組內程式碼彼此的相關程度,越高越好。
耦合度表示模組彼此的依賴程度,越低越好。
有個簡單的判斷方法。
| 問題 | 訊號 |
|---|---|
| 修改一個功能時,是否要同時修改多個模組? | 耦合度高 |
| 模組中是否混有彼此無關的程式碼? | 內聚力低 |
| 刪除一個模組後,是否只會消失那項功能? | 拆分良好 |
核心標準是變更。一起變更的程式碼放在同一模組,因不同原因變更的程式碼放在不同模組。
這個原則很熟悉吧?SOLID的單一職責原則(SRP)所說的「變更原因」,被延伸到了模組層級。
模組化有什麼好處?
變更成本會最先降低。
有了邊界,修改的影響範圍就能限制在模組內。只要模組的公開介面沒變,審查者也能判斷外部是安全的。
團隊分工也會更容易。按模組分配負責範圍,可減少工作區域重疊造成的衝突。
測試也更輕量。可以單獨測試一個模組,不必啟動整個 App 就能驗證。
建置速度也會改善,因為只需重新編譯變更過的模組。不過這是結果,不是目的。邊界設計糟糕時,仍可能每次都重新建置全部內容。
在Swift中,邊界會透過存取控制子呈現。
// 只在模組外公開協定
public protocol PriceFormatter {
func format(_ amount: Int) -> String
}
// 將實作隱藏在模組內
final class KoreanPriceFormatter: PriceFormatter {
func format(_ amount: Int) -> String { "\(amount)圓" }
}
在外部只要知道PriceFormatter即可。理想狀態是,即使替換內部實作,也不必重新編譯模組外的程式碼。
何時該拆分,何時該忍住?
模組化不是免費的。建立邊界會帶來介面設計、版本管理與專案設定成本。
| 情況 | 判斷 |
|---|---|
| 修改一個功能時,多個團隊或領域的程式碼都會一起變更 | 該拆分了 |
| 建置緩慢,經常打斷開發流程 | 可以拆分,但先設計變更邊界 |
| 領域邊界仍經常變動的早期產品 | 先忍住,等邊界穩定後再拆 |
| 一個人製作的小型 App | 整理資料夾並使用存取控制子就足夠 |
邊界畫錯的模組化比不做更糟。如果兩個模組總是一起變更,表示邊界錯了,應該合併。
面試時會這樣問
Q. 請說明內聚力與耦合度,以及兩者的關係。
內聚力是模組內元素的相關性,耦合度是模組間的依賴程度。良好設計追求高內聚力與低耦合度。把相關程式碼集中後,與模組外的互動自然減少,因此兩者具有互補關係。
Q. 你會以什麼標準拆分模組?
以變更原因為標準。同因一起變更的程式碼放在同一模組,因不同理由變更的程式碼則分開。能回答「這個模組隱藏了什麼設計決策」,才是正確的邊界。
下一篇將探討拆分模組後必然遇到的問題:模組間的相依方向與循環相依。
比拆分更難的是拆分後的關係設計。帶著內聚力與耦合度的感覺閱讀,會容易許多。

