軟體設計

【模組化 #1】什麼是模組化?拆分程式碼的真正原因(內聚力・耦合度總整理)

專案越大,修改一個檔案就越令人害怕,因為無法預估影響會擴散到哪裡。

閱讀 4 分鐘
【模組化 #1】什麼是模組化?拆分程式碼的真正原因(內聚力・耦合度總整理) 封面圖

專案越大,修改一個檔案就越令人害怕,因為無法預估影響會擴散到哪裡。

建置速度變慢,程式碼審查範圍變廣。新成員也會苦惱該從哪裡開始閱讀。

模組化是解決這個問題最古老的方法。簡單說,就是把大型程式碼區塊拆成能獨立理解與替換的單位。

本文將整理模組化究竟在拆分什麼、以內聚力和耦合度判斷好模組的標準,以及何時該拆分。

先看重點摘要。

  1. 模組化是把「一起變更的東西」放在一起,建立邊界
  2. 好模組的標準是高內聚力、低耦合度
  3. 目的首先是降低變更成本,而不是提升建置速度
  4. 在邊界不清楚時拆分,反而只會增加複雜度

模組化究竟在拆分什麼?

拆分資料夾和拆分模組是兩回事。

資料夾只是整理檔案。任何資料夾中的程式碼都能自由使用其他資料夾的程式碼。

模組則會形成具強制力的邊界。在模組外,只能使用該模組公開的介面。

模組化的本質不是整理檔案,而是讓編譯器強制區分「外部需要知道的事」和「不需要知道的事」。

有了這個邊界,就能做到兩件事。

  • 獨立理解:不必了解內部,只看介面就能使用
  • 獨立替換:介面相同時,即使整個替換內部實作,外部也不受影響

這就是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. 你會以什麼標準拆分模組?

以變更原因為標準。同因一起變更的程式碼放在同一模組,因不同理由變更的程式碼則分開。能回答「這個模組隱藏了什麼設計決策」,才是正確的邊界。


下一篇將探討拆分模組後必然遇到的問題:模組間的相依方向與循環相依。

比拆分更難的是拆分後的關係設計。帶著內聚力與耦合度的感覺閱讀,會容易許多。

延伸閱讀