🎯 什麼情境該想到我

當你「同一個詞(例如『客戶』『商品』)在系統不同部分其實指不一樣的東西,或一個模型越長越大、概念開始互相打架」的時候。你需要明確畫出「這套模型和語言到底適用在哪個範圍」。

⚙️ 怎麼用(步驟 / 公式)

意圖:明確界定「一個特定領域模型所適用的邊界」。在界內,模型與通用語言保持嚴格一致;跨出邊界,同一個詞可以有不同意義,模型也不必相同。

做法要點:

  1. 找出模型會產生矛盾、或語言開始分歧的地方,那通常就是上下文邊界。
  2. 為每個限界上下文明確命名,界定它涵蓋哪些概念、由誰負責、對應哪塊程式碼/資料庫。
  3. 界內維持單一一致的模型與語言;不同上下文之間的同名概念,透過翻譯而非直接共用來連接。
  4. 讓邊界對應到團隊、部署單元或程式碼結構,使一致性有人守得住。

★★ 怎麼「找到」邊界:五個可觀察的訊號

① ★★ 同一個名詞的定義不一致(最可靠的訊號)
   ★ 「訂單」在出貨眼中是「一箱東西 + 地址」,在計費眼中是「一筆金額 + 稅務」
   → ★★ 你發現自己在說「這裡的訂單是指…」時,邊界就在那裡

② ★★ 一個 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 的紀律優勢。

  • 邊界靠團隊紀律與翻譯機制守住;沒有機制時概念會悄悄滲漏,退化成 大泥球。

🔗 相關工具