🎯 什麼情境該想到我
當你「同一個詞(例如『客戶』『商品』)在系統不同部分其實指不一樣的東西,或一個模型越長越大、概念開始互相打架」的時候。你需要明確畫出「這套模型和語言到底適用在哪個範圍」。
⚙️ 怎麼用(步驟 / 公式)
意圖:明確界定「一個特定領域模型所適用的邊界」。在界內,模型與通用語言保持嚴格一致;跨出邊界,同一個詞可以有不同意義,模型也不必相同。
做法要點:
- 找出模型會產生矛盾、或語言開始分歧的地方,那通常就是上下文邊界。
- 為每個限界上下文明確命名,界定它涵蓋哪些概念、由誰負責、對應哪塊程式碼/資料庫。
- 界內維持單一一致的模型與語言;不同上下文之間的同名概念,透過翻譯而非直接共用來連接。
- 讓邊界對應到團隊、部署單元或程式碼結構,使一致性有人守得住。
★★ 怎麼「找到」邊界:五個可觀察的訊號
① ★★ 同一個名詞的定義不一致(最可靠的訊號)
★ 「訂單」在出貨眼中是「一箱東西 + 地址」,在計費眼中是「一筆金額 + 稅務」
→ ★★ 你發現自己在說「這裡的訂單是指…」時,邊界就在那裡
② ★★ 一個 struct 有一半的欄位對某群使用者永遠是 nil
★ Order 有 ShippingAddress(計費不用)與 TaxID(出貨不用)
→ ★ 這是 [[暫時欄位]] 在跨上下文層級的樣貌
③ ★★ 用 git 看「哪些檔案總是一起改」
git log --name-only | ★ 找出時間耦合的檔案群
→ ★ 那些群就是天然的上下文候選(比任何架構圖準)
④ ★ 團隊邊界
★ 兩個團隊各自改同一個模組 ⇒ ★★ 要嘛合併團隊,要嘛切開模組([[康威定律作為設計工具]])
⑤ ★ 變化節奏不同
★ 一個月改三次 vs 一年改一次 → ★★ 綁在一起讓穩定的那部分被迫跟著發布
★★ ① 和 ③ 是最實用的兩個:① 來自對話,③ 來自資料。
★ 兩者指向同一個地方時,那個邊界幾乎確定是對的。
★★ Go 的落地:上下文邊界 = package 邊界(而且它是編譯器強制的)
internal/
ordering/ ★ 一個限界上下文
order.go ★ 這裡的 Order 有 lines、status
service.go
billing/ ★ 另一個限界上下文
invoice.go ★★ 這裡也有一個「Order」,但它只有 amount 與 taxID
customer.go ★ 而這裡的 Customer 和 ordering 的 Customer 是不同型別
shipping/
shipment.go
★★ 三個 Go 特有的優勢:
① ★★ package 是編譯器強制的邊界 —— 跨 package 只能用匯出的 API
→ ★ 這讓「上下文邊界」不是文件上的約定,而是編譯錯誤
② ★★ internal/ 可以建立更強的邊界
★ internal/ordering/internal/… 只有 ordering 子樹能 import
③ ★★ 循環 import 是硬性編譯錯誤
→ ★ 它會強迫你想清楚依賴方向(★ 這是 Java 沒有的紀律)
★ 用 depguard 把邊界寫進 CI:
depguard:
rules:
ordering:
files: ["**/internal/ordering/**"]
deny: [{ pkg: "myapp/internal/billing" }] # ★★ 禁止直接 import 別的上下文★★ 這一段設定比任何架構文件有效——它讓「邊界被穿透」變成 CI 紅燈。
★★ 同名型別「刻意重複」是對的
// ★ ordering 的 Order
package ordering
type Order struct { id ID; lines []Line; status Status }
// ★★ billing 的 Order —— 刻意是另一個型別
package billing
type Order struct { id ID; amount Money; taxID TaxID }★★ 這看起來像 重複程式碼,但它不是:
★ 判準(見 [[工具-DRY原則]]):這兩個會因為「同一個原因」而一起改嗎?
★ 出貨規則改 → 只改 ordering
★ 稅法改 → 只改 billing
→ ★★ 不會一起改 ⇒ 它們是兩個概念,剛好共用一個名字
→ ★ 合併它們 = 一個服務兩邊的上帝型別(見 [[內幕交易]] 的 common package)
★★ 邊界該多大:兩個方向的失敗
★★ 切太細:
① 每次功能都要跨三個上下文 ⇒ ★ 大量整合成本與最終一致性
② ★★ 交易邊界被迫跨上下文 ⇒ Saga、補償、雙寫(見 [[工具-分散式事務的取捨]])
③ ★ 每個上下文只有一個人懂
★★ 切太粗:
① ★ 模型內部自相矛盾(「這裡的訂單是指…」)
② ★★ 兩個團隊在同一個模組上互相踩([[霰彈式修改]])
③ ★ 穩定的部分被迫跟著不穩定的部分發布
★★ 判準:一個「典型的需求變動」需要跨幾個上下文?
一個 → 邊界對了。
三個以上 → ★ 切太細,考慮合併。
★ 而且:限界上下文 ≠ 微服務。 ★★ 一個部署單元裡可以有五個限界上下文
(模組化單體)——★ 先切模型邊界,部署邊界是後來才決定的事
(見 工具-服務邊界與演進式拆分)。
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
邊界切太細會讓整合成本暴增;切太粗又會讓模型內部自相矛盾,需要權衡。
-
限界上下文是「模型適用範圍」的概念邊界,未必等於子領域(問題空間);別把它和微服務切法直接畫等號。
-
邊界要靠團隊紀律與翻譯機制守住,否則概念會悄悄滲漏、退化成大泥球。
-
★★ 限界上下文 ≠ 微服務。 一個部署單元裡可以有五個限界上下文(模組化單體)。先切模型邊界,部署邊界是後來的事。
-
★★ 同名型別刻意重複是對的。判準是「會不會因為同一個原因一起改」——不會 → 它們是兩個概念,合併會得到上帝型別。
-
★★ 最可靠的兩個邊界訊號:同一個名詞定義不一致(來自對話)+ git 上「總是一起改的檔案群」(來自資料)。兩者指向同一處就幾乎確定。
-
★★ 用
depguard把邊界寫進 CI,讓「邊界被穿透」變成紅燈。這比任何架構文件有效。 -
★ 判準:一個典型的需求變動要跨幾個上下文? 三個以上 → 切太細。
-
★ 跨邊界要做明確轉換(防腐層),不要共用型別。
-
★ Go 的 package 是編譯器強制的邊界,循環 import 是硬性錯誤 —— 這是 Go 相對 Java 的紀律優勢。
-
邊界靠團隊紀律與翻譯機制守住;沒有機制時概念會悄悄滲漏,退化成 大泥球。
🔗 相關工具
- 通用語言 Ubiquitous Language(語言的一致性以此為界)
- 上下文對應 Context Map(把各限界上下文與其關係畫出來)
- 大泥球 Big Ball of Mud(邊界不清、模型混亂區塊的典型下場)
- 領域驅動設計
- 通用語言 —— 語言的一致性以此為界
- 上下文對應 —— 把上下文與關係畫出來
- 防腐層、對應器 —— 跨邊界的翻譯
- 大泥球 —— 邊界失守的下場
- 核心子領域 —— 問題空間 vs 解答空間
- 工具-服務邊界與演進式拆分 —— 何時把模型邊界升級成部署邊界
- 康威定律作為設計工具 —— 團隊邊界與模組邊界
- 工具-DRY原則 —— 刻意重複的判準