🎯 什麼情境該想到我
★ 分工:這頁講「模組邊界怎麼切、名字怎麼取」;
限界上下文 講「更大的邊界:模型與語言的適用範圍」。
★ 一個限界上下文裡可以有多個模組。
當模型變大、類別一多就難以掌握,你想把相關的領域概念分組、讓人能一次理解一塊,並降低不同區塊間的糾纏時。
⚙️ 怎麼用(步驟 / 公式)
意圖:以高內聚、低耦合的方式把領域概念分組,讓模型可被分塊理解。
- 依領域概念的內聚性分組:關係緊密、常一起變動的放同一模組。
- 追求模組內高內聚、模組間低耦合——模組邊界應對應概念邊界,而非技術分層。
- 用通用語言替模組命名:模組名本身就是領域語言的一部分,看名字就懂它負責什麼。
- 隨著理解加深持續重構模組邊界,別讓它僵在最初的猜測。
★★ Evans 的核心主張:模組名是通用語言的一部分
★★ 「模組」在 DDD 裡不是技術分組,而是★ 「領域概念的分群」。
→ ★ 所以模組名應該是領域專家聽得懂的詞:
✅ ordering / billing / shipping / pricing / inventory
❌ ★★ models / services / handlers / utils / helpers / common
★★ 按技術分層(models/services/handlers)是這一頁反對的核心:
★ 它讓「加一個功能」要動三個資料夾(霰彈式修改),
而且從目錄結構完全看不出這個系統在做什麼。
★ 打開internal/應該像看一份業務功能清單,不是一份技術詞彙表。
★★ Go 的落地:package 就是模組,而且它是編譯器強制的
internal/
ordering/ ★ 領域概念
order.go
service.go
repo.go ★★ 同一個概念的領域、應用、持久化放在一起
billing/
shipping/
pricing/
platform/ ★ 技術性的東西集中在一個明確標示的地方
db/
mail/
telemetry/
★★ 「按概念切、概念內才分層」是 Go 社群的主流,而它有三個具體好處:
① ★★ 刪除一個功能 = 刪除一個資料夾(★ 而不是在六個資料夾裡找)
② ★ 「這個概念用到了什麼」= 那個 package 的 import 清單
③ ★★ 循環 import 會告訴你「兩個概念糾纏了」(★ Java 不會)
★★ Go 的命名規則和 Java 相反的三點
① ★★ package 名已經是前綴 —— 不要重複
✅ ordering.Order、ordering.Service
❌ ★ ordering.OrderingOrder、ordering.OrderService(★★ 呼叫端看到 ordering.OrderService)
② ★★ package 名用單數、小寫、不用底線或駝峰
✅ ordering、pricing、httpapi
❌ ★ orderings、order_service、orderService
③ ★★ 不要 utils / common / helpers / base
→ ★ 它們沒有「一個概念」,所以會無限成長(見 [[內幕交易]])
→ ★★ 判準:★ 你能用一句話說出「這個 package 負責什麼」嗎?
不能 → ★ 它不該存在,把內容搬到真正屬於的地方
★★ ③ 的實務做法:★ 與其建 utils,
把那個函式放到「它操作的型別所在的 package」——★ 多數時候它本來就該在那裡
(見 依戀情結)。
★★ 模組邊界的三個可觀察訊號(和限界上下文同源但粒度更小)
① ★★ 循環 import(Go 直接編譯失敗)
→ ★ 它是最硬的訊號:★★ 兩個 package 互相需要 ⇒ 邊界錯了或該合併
→ ★ 解法見 [[搬移函式]]:介面定義在消費方、共用型別下移、或合併
② ★ import 數量與方向
★ 一個 package import 了十五個內部 package → ★★ 它可能是一個組裝點(main/wire),
★ 或它承擔了太多責任
→ ★ go list -deps 可以畫出實際的依賴圖
③ ★★ git 上「總是一起改」的 package
→ ★ 那兩個 package 之間有隱性耦合 ⇒ ★★ 要嘛合併,要嘛找出洩漏的抽象
★ 用 depguard 把允許的依賴方向寫進 CI(見 限界上下文)——
★★ 這讓模組邊界從「約定」變成「編譯/CI 錯誤」。
★★ 模組該多大:一個實用的下限與上限
★ 下限:★★ 一個 package 只有一個型別、一個檔案 → ★ 通常該合併
→ ★ Go 不鼓勵 Java 式的「一個類別一個檔案一個 package」
→ ★★ 例外:★ 為了建立編譯邊界而刻意隔離的聚合
★ 上限:★★ 一個 package 有二十個檔案、而它們分成三群互不相干 → ★ 該拆
→ ★ 判準:★★ 你能用一句話說出它負責什麼嗎?
「ordering 負責訂單的生命週期」✅
「core 負責…嗯…核心的東西」❌
★★ 最實用的檢查:★ 對每個 package 寫一行 doc comment。
// Package ordering 管理訂單從草稿到完成的生命週期,含品項、狀態轉移與取消規則。
package ordering★ 寫不出來的 package 就是邊界有問題的 package——
★★ 而這個檢查成本極低,而且 doc comment 本來就該有。
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
別只照技術層(controller/service/dao)切模組,那會把同一概念拆散、耦合反而升高。
-
模組邊界不是永久的:領域理解演進時要跟著調整。
-
模組是模型內的分組,範圍比限界上下文(Bounded Context)小,兩者別混淆。
-
★★ 按技術分層(models/services/handlers)是這一頁反對的核心。 它讓加一個功能要動三個資料夾,而且從目錄結構看不出系統在做什麼。
-
★★ 不要有
utils/common/helpers。 判準:能用一句話說出它負責什麼嗎?不能 → 它不該存在。 -
★★ package 名已經是前綴:
ordering.Order而非ordering.OrderingOrder;呼叫端看到的是ordering.OrderService這種結巴。 -
★★ 循環 import 是最硬的邊界訊號(Go 直接編譯失敗):兩個 package 互相需要 ⇒ 邊界錯了或該合併。
-
★★ 對每個 package 寫一行 doc comment —— 寫不出來的就是邊界有問題的。 成本極低。
-
★ 用
depguard把允許的依賴方向寫進 CI,讓邊界從約定變成錯誤。 -
★ Go 不鼓勵「一個類別一個 package」。 只有一個型別一個檔案的 package 通常該合併。
-
★ 與其建
utils,把函式放到「它操作的型別所在的 package」——多數時候它本來就該在那裡。 -
模組名是通用語言的一部分,應該是領域專家聽得懂的詞。