🎯 什麼情境該想到我
當你「兩個團隊/上下文之間有一小塊領域模型與程式碼高度相同,各做各的會重複又容易分歧」的時候。你想把這一小塊明確劃為共用,但也接受它會綁住雙方。
⚙️ 怎麼用(步驟 / 公式)
意圖:兩個團隊協議「共用一小塊領域模型與其程式碼」(包含相關的資料庫設計)。這一塊是共享核心,任何一方都不得擅自更動,改動必須雙方協調並一起測試。
做法要點:
- 明確界定共享核心的範圍——刻意保持「小」,只放真正需要共用的核心概念。
- 約定:任何一方要改共享核心,都要先與另一方溝通、取得同意。
- 每次變更後跑雙方共同的測試,確保兩邊都仍然可用。
- 定期一起檢視共享核心是否還值得共享,或該退場改成翻譯式整合。
★★ 它是九種關係裡「最危險」的一種,而它的危險是可以預測的
★ 共享核心(Shared Kernel)= 兩個上下文共用一部分「模型與程式碼」
→ ★★ 而共用程式碼意味著:
① ★★ 任何一方改它,另一方立刻受影響(★ 沒有翻譯層緩衝)
② ★★ 它必須同時滿足兩邊的需求 ⇒ ★ 它會變成兩邊需求的聯集
③ ★★ 沒有人單獨擁有它 ⇒ ★ 改動需要協調,而協調成本會持續上升
★★ 這正是 內幕交易 裡「
commonpackage 一定會腐敗」的機制,
★ 而 Evans 自己就警告過:共享核心必須「刻意保持小」,而且變更需要雙方同意。
★★ 它腐敗的典型過程(可預測,所以可預防)
★ 第 1 週:★ 兩邊都需要 Money 與 Address → ★ 放進 shared/
★ 第 6 週:★ 計費需要 Money.WithTax() → ★ 加上去(★★ 訂單不需要)
★ 第 12 週:★ 訂單需要 Address.IsRemoteArea() → ★ 加上去(★★ 計費不需要)
★★ 第 30 週:shared/ 有 40 個型別、120 個方法,
★ 而每個上下文只用得到 30%,★★ 改任何東西都要跑兩邊的測試
★★ 第 52 週:★ 沒有人敢刪任何東西,因為不知道誰在用
★ 關鍵觀察:★★ 每一步都是「合理」的,而累積的結果是不可接受的。
→ ★ 所以需要一個機制,而不是靠紀律。
★★ 三個可執行的防腐機制
① ★★ 只放「定義穩定、範圍窄的小片語」
✅ ★ Money、Address、DateRange、CustomerRef、TimeZone
❌ ★★ Order、Customer、Invoice(★ 完整的業務實體)
→ ★ 判準:★★ 這個型別的「形狀」會因為業務需求而改變嗎?
會 → ★ 不要共享(見 [[標準資料模型]] 的同一判準)
② ★★ 共享核心要有一個明確的擁有者與變更流程
★ CODEOWNERS 檔案 + ★「加欄位需要兩邊 approve」的規則
→ ★★ 沒有擁有者的共享核心就是無主的 common package
③ ★★ 用 CI 量它的大小,並設一個上限
★ 「shared/ 的型別數超過 15 個就要 review」
→ ★ 這聽起來粗糙,★★ 但它是唯一會在腐敗發生時發出訊號的機制
★★ ① 的判準最實用:★ Money 的形狀(金額 + 幣別)十年不會變;
Order 的形狀每季都在變——所以前者可以共享,後者不行。
★★ Go 的落地:共享核心是一個 package,而且要能被量測
internal/
sharedkernel/ ★★ 名字要誠實 —— 不要叫 common 或 core
money.go ★ Money(形狀穩定)
address.go ★ Address(形狀穩定)
ids.go ★ 具名 ID 型別
ordering/ ★ import sharedkernel ✅
billing/ ★ import sharedkernel ✅
★★ 三個 Go 特有的注意點:
① ★★ 名字要誠實叫 sharedkernel,不要叫 common/core/pkg
→ ★ 誠實的名字讓「往裡面加東西」變成一個需要解釋的動作
② ★★ 用 depguard 限制它「不能 import 任何上下文」
depguard:
sharedkernel:
files: ["**/internal/sharedkernel/**"]
deny: [{ pkg: "myapp/internal/ordering" }, { pkg: "myapp/internal/billing" }]
→ ★ 它只能被依賴,不能依賴別人(★★ 否則就是循環耦合)
③ ★ 共享核心不該有 I/O、不該有 db、不該有 http
→ ★★ 它是純的值型別與純函式;有 I/O 就代表它是一個服務,不是核心
★★ 什麼時候該用它 vs 該改成 防腐層
| 選共享核心 | 選 防腐層 |
|---|---|
| ★ 型別形狀穩定(Money、Address) | ★★ 型別形狀會因業務而變(Order、Customer) |
| ★ 兩邊的語義相同 | ★★ 語義不同(「訂單」在兩邊是兩件事) |
| ★ 兩隊有良好的協調機制 | 協調困難或跨組織 |
| ★ 共享部分很小(< 10 個型別) | 需要共享的東西很多 → ★★ 那訊號是「該合併或該重劃邊界」 |
★★ 最重要的一條:★ 「需要共享的東西很多」不是「加大共享核心」的理由,
而是「邊界劃錯了」的訊號。
→ ★ 這時要問:★★ 這兩個上下文是不是其實該是一個?(見 限界上下文)
★ 兄弟辨析
| 差別 | |
|---|---|
| 合作關係 | ★ 協調但不共用程式碼 |
| 防腐層 | ★★ 不共用,各自翻譯 —— ★ 貴但自主 |
| 發布語言 | ★ 共用「格式」而非「程式碼」(★ 耦合輕得多) |
| 標準資料模型 | ★ 同一個陷阱的企業級版本 |
| 內幕交易 | ★ 共用程式碼腐敗的機制 |
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
共享核心讓兩隊緊密耦合,需要高頻協調與良好關係;團隊若無法密切合作就別用。
-
範圍會不知不覺膨脹,要刻意守住「小」,否則等於兩隊共用一個大模型、失去獨立性。
-
★★ 它是九種關係裡最危險的一種,因為共用程式碼沒有翻譯層緩衝。
-
★★ 腐敗過程可預測(每一步都合理,累積不可接受),所以需要機制而不是紀律。
-
★★ 只放「形狀穩定」的小片語(Money、Address、DateRange)。判準:這個型別的形狀會因業務需求而改變嗎?會 → 不要共享。
-
★★ 名字要誠實叫
sharedkernel,不要叫common/core/pkg—— 誠實的名字讓「往裡面加東西」變成需要解釋的動作。 -
★★ 用
depguard限制它不能 import 任何上下文。 它只能被依賴,不能依賴別人。 -
★★ 「需要共享的東西很多」不是加大共享核心的理由,而是邊界劃錯的訊號。
-
★ 共享核心要有 CODEOWNERS 與「加欄位需要雙邊 approve」的規則,否則它是無主的 common package。
-
★ 用 CI 量它的大小並設上限——這是唯一會在腐敗發生時發出訊號的機制。
-
共享核心不該有 I/O、db、http。 有的話它是一個服務,不是核心。
🔗 相關工具
- 合作關係 Partnership(另一種需要高度協調的緊密關係)
- 客戶-供應商 Customer-Supplier(改用上下游而非共用的整合方式)
- 各行其道 Separate Ways(當共享不再划算時的退場選項)
- 領域驅動設計
- 合作關係 —— 協調但不共用程式碼
- 防腐層 —— ★ 不共用,各自翻譯
- 發布語言 —— 共用格式而非程式碼
- 標準資料模型 —— 企業級版本的同一陷阱
- 內幕交易 —— 腐敗的機制
- 限界上下文 —— 「該不該合併」的判準
- 值物件(DDD)、金錢 —— 適合放進共享核心的東西