🎯 什麼情境該想到我
當你「兩個上下文是明確的上游供給、下游消費關係,而下游的需求該被上游正式納入規劃」的時候。你想把這種依賴變成有約定、可協商的關係,而不是下游單方面被動承受。
⚙️ 怎麼用(步驟 / 公式)
意圖:確立上游(供應商)與下游(客戶)的關係,讓下游的需求被上游納入規劃與優先序,並以自動化測試把介面約定固定下來,讓上游能安心變更。
做法要點:
- 明確指定誰是上游供應商、誰是下游客戶,以及流動方向。
- 建立協商機制:下游把需求提入上游的規劃流程,雙方一起排優先序與時程。
- 以下游驅動的驗收測試把介面約定固化;上游改動要先通過這些測試。
- 把測試納入上游的持續整合,讓上游能在不驚動下游的前提下自由演進內部。
★★ 它的核心是一個「義務」:上游必須為下游的需求排程
★ 客戶-供應商(Customer/Supplier)的定義:
★★ 上游(供應商)有義務考慮下游(客戶)的需求
→ ★ 三個具體的義務:
① ★★ 排程協商 —— ★ 下游的需求會進上游的 backlog
② ★★ 回歸測試 —— ★ 上游改動前要跑下游提供的測試
③ ★ 變更通知 —— ★ 破壞性變更要提前告知
★★ 判準:下游能不能把一張 ticket 放進上游的 backlog,而且它會被排?
可以 → ★ 這是客戶-供應商。
不行 → ★★ 那是 追隨者(★ 你只能照抄它給你的東西)。
★ 這個判準是組織性的,不是技術性的——★★ 而這正是 Evans 的重點。
★★ 讓它真正運作的關鍵機制:消費者驅動契約測試
★★ 「回歸測試」這個義務,如果沒有機制就是空話。
→ ★ 具體做法:★★ 下游提供一組測試,上游的 CI 必須跑它們
// ★ 下游(客戶)維護一組「我依賴你的什麼」的測試
// ★★ 它們放在上游的 repo 裡,或用 Pact 之類的 broker 共享
func TestOrderingAPIContract(t *testing.T) {
// ★ 我依賴:GET /orders/{id} 回傳的 JSON 有 id、status、total_cents 三個欄位
// ★★ 我不依賴:其他欄位(★ 所以上游可以自由增刪它們)
resp := get(t, "/orders/ord_1")
assertHasFields(t, resp, "id", "status", "total_cents")
}★★ 這組測試最重要的性質是「它只斷言下游真正用到的東西」:
★ 斷言整個 JSON 相等 → ★★ 上游加一個欄位就紅燈 ⇒ 上游會開始討厭這組測試
★ 只斷言用到的欄位 → ★ 上游可以自由演進 ⇒ ★★ 契約測試才活得下去
★ 這是契約測試成敗的關鍵細節,而它常被寫錯。
★★ 三個實務上的摩擦點,以及各自的解法
① ★★ 上游不知道「誰在用什麼」
★ 於是不敢刪任何欄位(★ API 只增不減,十年後有 80 個欄位)
→ ✅ ★ 契約測試就是答案:★★ 沒有測試覆蓋的欄位就可以刪
→ ★ 或最低成本版:★ 對每個欄位加 usage metric(★ 三個月沒人讀就刪)
② ★★ 下游的需求排不進上游的 backlog
★ 上游有自己的 OKR,而下游的需求「對他們沒有價值」
→ ✅ ★ 這需要組織介入(★ 共同的目標、或把下游需求算進上游的 KPI)
→ ★★ 技術解決不了這個問題 —— ★ 而假裝能解決會讓關係退化成 [[追隨者]]
③ ★ 破壞性變更的通知期
→ ★★ 要有一個明確的數字(★ 「破壞性變更提前兩個 sprint 通知」)
→ ★ 沒有數字的「會提前通知」等於沒有承諾
★★ ② 是最根本的:★ 客戶-供應商關係的存續依賴組織的支持,
而不是技術機制。→ ★★ 沒有組織支持時,誠實地把它畫成 追隨者 或 防腐層。
★★ 上游該提供什麼:三個層次
★ ① 最低:★ 一份穩定的 API + 版本策略
★ ② 中等:★★ + 消費者驅動契約測試 + 破壞性變更的通知期
★★ ③ 完整:★ 以上 + [[開放主機服務]](★ 一個為多個下游設計的穩定協定)
+ [[發布語言]](★ 有版本的交換格式)
★ 判準:下游有幾個?
一個 → ★ ② 就夠。
多個 → ★★ 升級成 開放主機服務,否則你會為每個下游各自客製。
★★ 下游該做什麼(這一頁常被寫成只有上游的義務)
① ★★ 明確表達「我依賴什麼」(★ 就是契約測試)
→ ★ 沒有這個,上游只能猜,而猜的結果是「什麼都不敢改」
② ★★ 不要依賴未公開的行為
★ 「這個欄位雖然沒寫在文件裡,但它總是有值」→ ★★ 那不是契約
→ ★ 依賴它,你就失去了抱怨的立場
③ ★ 自己做一層薄的隔離
→ ★★ 即使是客戶-供應商,★ 在邊界上做一次轉換仍然值得
(★ 一個 [[對應器]],不必是完整的 [[防腐層]])
→ ★ 這讓「上游改了一個欄位名」的影響範圍是一個檔案,不是三十個
★★ ③ 是投資報酬率最高的一項:★ 成本是一個對應函式,
收益是「上游的變更不會擴散到你的領域層」。
★ 兄弟辨析
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
一個上游有多個下游時,需求會互相衝突,優先序協調成本上升。
-
過度綁定下游的驗收測試也可能拖慢上游,要平衡「約定」與「上游自主」。
-
★★ 判準:下游能不能把 ticket 放進上游的 backlog,而且它會被排? 不行 → 那是 追隨者,不是客戶-供應商。
-
★★ 契約測試只能斷言「下游真正用到的欄位」。 斷言整個 JSON 相等 → 上游加欄位就紅燈 → 上游會討厭這組測試 → 契約測試死掉。
-
★★ 「下游需求排不進上游 backlog」是組織問題,技術解決不了。 假裝能解決會讓關係退化成 追隨者。
-
★★ 破壞性變更的通知期要有明確數字。 沒有數字的「會提前通知」等於沒有承諾。
-
★★ 契約測試同時解決「上游不敢刪欄位」的問題:沒有測試覆蓋的欄位就可以刪。
-
★ 下游也有義務:明確表達依賴什麼、不依賴未公開的行為、在邊界做一層薄的轉換。
-
★ 在邊界做一次轉換(一個 對應器)是投資報酬率最高的一項——上游改欄位名的影響範圍是一個檔案。
-
★ 下游超過一個 → 升級成 開放主機服務,否則會為每個下游各自客製。
🔗 相關工具
- 追隨者 Conformist(上游不配合時,下游只能順從的退化情形)
- 防腐層 Anticorruption Layer(下游自保、隔離上游變動的手段)
- 開放主機服務 Open Host Service(上游服務多個下游時的做法)
- 領域驅動設計
- 合作關係 —— 對等、共同存亡
- 追隨者 —— 上游沒有配合義務
- 防腐層 —— 下游單方面付出翻譯成本
- 開放主機服務、發布語言 —— 多個下游時的升級
- 對應器 —— 邊界上的薄轉換
- 上下文對應 —— 九種關係的總覽
- 工具-資料編碼與演進 —— 版本策略