大多數進行模組化的團隊,到了某個階段都會建立名為 Common、Core 或 Utils 的模組。
「很多地方會用到,先放這裡」的判斷不斷累積後,這個模組不知不覺成了專案中最大、變動最頻繁、也最受所有人相依的模組。
先說結論:名為 Common 的模組,就像是在承認它沒有內聚力。「共用」不能成為整理程式碼的依據。
本文整理 Common 模組為何會讓模組化倒退,以及如何透過圖層結構(垂直分離)與功能結構(水平分離)來預防。
Common 模組為什麼危險?
問題會分成三個階段發展。
第一階段是變成垃圾桶。「不知道該放在哪裡的程式碼」全都進入 Common。日期格式化工具旁邊放著網路用戶端,旁邊又放著自訂按鈕。彼此毫無關聯。
第二階段是所有模組都相依。每個功能模組都會 import Common。此時 Common 位於相依圖最下游,也就是應該最穩定的位置。
第三階段是整體重新建置。但 Common 是垃圾桶,因此反而變動最頻繁。上一篇文章的穩定相依原則被完全顛倒了。只改一個按鈕顏色,所有模組都要重新建置。
當被最多模組相依的模組,變成變動最頻繁的模組時,模組邊界就形同虛設。
圖層結構:垂直切分
預防措施的第一個軸線,是依照職責進行垂直分離。常見的 3~4 層結構如下。
| 圖層 | 內容 | 範例 |
|---|---|---|
| Feature | 畫面/功能單位模組 | 首頁、搜尋、訂單、設定 |
| Domain | 業務規則/模型/使用案例 | 訂單政策、會員模型 |
| Core | 特定技術的薄包裝器 | 網路、儲存、記錄 |
| Shared | 真正通用的工具 | 日期格式化工具、字串延伸 |
規則只有一個:相依性只能由上往下流動。
Feature 可以使用 Domain 和 Core,但 Core 不能知道 Feature。Feature 之間也不直接相互參照,而是由上層 App target 組裝。
這裡要注意它和 Common 的差異。Core 不是一個巨大的模組,而是依技術單位拆分成多個模組。
- 例如 CoreNetwork、CoreStorage、CoreLogging,分別都是獨立模組。
- 如果搜尋功能只使用記錄功能,就只 import CoreLogging。
如此一來,即使修正網路程式碼,只使用記錄功能的模組也不會重新建置。
功能結構:水平切分
第二個軸線,是依照網域進行水平分離。即使在同一個圖層內,首頁、搜尋、訂單也應該是不同模組。
判斷標準就是第一篇提過的問題:「這些程式碼會因為相同的理由而變更嗎?」
- 如果搜尋政策變更時不需要修改訂單程式碼,它們就是不同模組。
- 如果訂單畫面和訂單使用案例總是一起變更,繼續垂直拆分可能就太過頭了。
將垂直(圖層)與水平(功能)交疊後,就會形成網格。實際的模組會成為其中一格,例如「訂單功能的 Domain」或「搜尋功能的 Feature」。
如果 Common 已經變得過於龐大?
不需要一次摧毀既有的 Common。實務上驗證過的順序如下。
- 禁止新增:先訂下從今天起不再把新程式碼放入 Common 的規則。
- 調查使用處:從變動最頻繁的程式碼開始,確認它實際在哪裡使用。
- 找到歸屬:只在一個功能使用,就移到該功能模組;若是特定技術的包裝器,就移到 Core 系列模組。
- 重新命名剩餘內容:只將最後留下的真正通用程式碼,以 Shared 之類更精確的名稱隔離。
這項工作需要數個月,但可以在每個階段明顯看到重新建置範圍縮小。
什麼時候需要這種結構,什麼時候又會太過頭?
- 開發者達 4~5 人以上、功能領域達 3 個以上時:非常值得建立圖層規則。
- 如果是 1~2 人的專案:從 Feature/Shared 兩層開始即可。先畫出網格會太過頭。
- 不論規模大小:建議從一開始就遵守「不建立名為 Common・Utils 的模組」這項規則。
面試時可以這樣問
Q. 共用模組(Common)變大會產生哪些問題?應該如何設計?
所有人都相依的模組變成變動最頻繁的模組,因此即使是小修改,也需要整體重新建置與廣泛的影響評估。這是穩定相依原則被顛倒的狀態。應依職責細分為網路、儲存、記錄等模組,只將真正通用的程式碼隔離在最小的 Shared 模組中。
Q. 請說明將模組結構分成圖層時的規則。
建立 Feature、Domain、Core 等職責導向的圖層,並只允許相依性由上往下單向流動。同一圖層的模組不能直接相互參照,而要在上層組裝層連接。遵守這項規則後,變更的影響範圍就能限制在上方。
以上就是模組化原理三部曲。掌握邊界、相依方向與圖層結構,就具備了所需的概念工具。
從下一篇開始,會將這些原則直接套用到 iOS 專案,先介紹如何使用 Swift Package 實際拆分模組。

