🎯 什麼情境該想到我
當你「身為上游,有很多下游都要跟你整合,如果替每個下游各開一套特製介面會爆炸」的時候。你想提供一套公開、穩定的協定/服務,讓所有下游照同一個介面來接。
⚙️ 怎麼用(步驟 / 公式)
意圖:上游把自己的能力定義成一套公開、正式、穩定的協定或 API(一個開放的服務),供多個下游共用整合,而非為每個下游量身客製。
做法要點:
- 盤點下游共同需要的能力,設計成一套通用、對外一致的服務介面。
- 正式定義並文件化這套協定;盡量保持穩定與向後相容。
- 讓所有下游透過同一套介面整合;有特殊需求時再以擴充處理,而非另開分身。
- 常搭配發布語言作為交換的資料格式,讓協定的語彙也公開且良好定義。
★★ 它是上游的責任:為「多個你不認識的下游」設計一個穩定協定
★ 判準:★★ 你有幾個下游?
★ 一個 → [[客戶-供應商]] 就夠(★ 為那一個客製是合理的)
★★ 三個以上、或「你不知道有誰在用」→ ★ 開放主機服務
→ ★ 因為:★★ 為每個下游各自客製,★ 成本是 N 倍,而且它們會互相牽制
★★ 它和「有一個 API」的差別在於三件事:
★ ① 協定是為通用需求設計的,不是某個下游的需求;
★ ② 它有明確的版本與相容性承諾;
★ ③ 特殊需求走擴充點,不是改協定。
★★ Go/工程上的五個具體要求
① ★★ 版本策略要明確且寫下來
★ URL 版本(/v1/orders)vs header 版本 vs 只做向後相容
→ ★ 推薦:★★ 「只做向後相容的加法;不相容就開 /v2」
→ ★ 而「向後相容」要有定義:★★ 加可選欄位 ✅;改欄位語義 ❌;刪欄位 ❌
② ★★ 契約要是機器可讀的
★ OpenAPI 或 .proto,★★ 而且它是「真相」而非「文件」
→ ★ 由它產生 server stub 與 client → ★★ 文件不可能過時
→ ★ CI 檢查:★★ oapi-codegen/protoc 產生的碼和 repo 裡的一致
③ ★★ 錯誤格式要統一且穩定
★ RFC 9457 的 Problem Details(application/problem+json)是現成的選擇
→ ★★ 下游最痛的不是「功能不夠」,★ 而是「每個端點的錯誤格式都不一樣」
④ ★★ 要有「誰在用什麼」的可觀測性
★ 按 client id + 端點 + 欄位的 usage metric
→ ★★ 沒有這個,★ 你永遠不敢刪任何東西(見 [[客戶-供應商]] 的契約測試)
⑤ ★ 擴充點而非分支
★ 下游需要一個特殊欄位 → ★★ 不要在協定裡加 if clientType == "X"
→ ★ 用 ★★ 「選擇性欄位展開」(?expand=lines,customer)或
★ 一個明確的擴充區(metadata jsonb)
★★ ③ 最被低估:★ 下游整合一個 API 時,
一半的工作量花在「搞清楚各種失敗長什麼樣」——★ 統一錯誤格式直接砍掉那一半。
★★ 它的核心張力:通用性 vs 你自己的模型
★★ 開放主機服務逼你把「對外協定」和「內部模型」分開,★ 而這是必要的:
★ 內部模型要能自由重構(★ 它是你的核心)
★★ 對外協定必須穩定(★ 它是別人的依賴)
→ ★ 所以一定要有一層 [[資料傳輸物件]] + [[對應器]]
→ ★★ 「直接把領域型別序列化出去」在開放主機服務裡是明確的錯誤
—— ★ 因為那讓「重構領域模型」變成「破壞所有下游」
★ 一個具體的檢查:★★ 你的領域型別上有 json tag 嗎?
有 → ★ 那你的內部模型已經是公開契約了(見 資料傳輸物件)。
★★ 什麼時候不要做開放主機服務
① ★★ 只有一個下游,而且它是你自己的前端
→ ★ 那是 BFF(見 [[遠端外觀]]),★★ 而 BFF 應該為那個前端客製
→ ★ 硬做成「通用協定」會讓前端拿到一堆不需要的欄位、要打三次請求
② ★ 下游的需求差異極大
→ ★★ 一個「通用」協定會退化成所有需求的聯集
(★ 見 [[標準資料模型]] 的同一陷阱)
→ ★ 那時「每個下游一個 BFF」比一個開放主機服務更好
③ ★ 你還在探索邊界
→ ★★ 太早承諾穩定協定 ⇒ ★ 你會被自己的早期錯誤綁住
→ ★ 先 [[客戶-供應商]],等模式穩定了再升級
★★ ① 是最常見的誤用:★ 為了「架構正確」而把一個只服務自家前端的 API
做成通用協定——★★ 結果是前端體驗變差,而通用性沒有任何人受益。
★ 兄弟辨析
| 差別 | |
|---|---|
| 發布語言 | ★ 強調「格式」;開放主機服務強調「服務與協定」(★ 常一起出現) |
| 客戶-供應商 | ★ 為特定下游客製 |
| 遠端外觀 | ★ BFF:為特定客戶端設計 |
| 標準資料模型 | ★ 過度通用化的陷阱 |
| 防腐層 | ★ 下游端的對應措施(★ 有好的開放主機服務時可以省掉) |
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
設計一套「對所有人都好」的通用協定比針對單一下游難;下游只有一個時未必值得。
-
對外協定一旦公開就得維持穩定,改動受限、演化變慢,需要版本策略。
-
若你就是想暴露自家內部模型,容易讓下游被你的模型綁死;介面應為對外設計而非內部翻版。
-
★★ 判準:你有幾個下游? 一個且是自家前端 → 那是 BFF,應該客製。硬做成通用協定會讓前端體驗變差而沒有人受益。
-
★★ 「直接把領域型別序列化出去」是明確的錯誤 —— 那讓「重構領域模型」變成「破壞所有下游」。檢查:領域型別上有
jsontag 嗎? -
★★ 統一的錯誤格式是最被低估的一項。 下游整合的一半工作量花在「搞清楚各種失敗長什麼樣」。用 RFC 9457 Problem Details。
-
★★ 契約要是機器可讀且是「真相」(由 OpenAPI/.proto 產生 stub),並在 CI 檢查產生的碼與 repo 一致——這樣文件不可能過時。
-
★★ 要有「誰在用什麼」的 usage metric,否則你永遠不敢刪任何東西。
-
★ 特殊需求走擴充點,不要在協定裡加
if clientType == "X"。 -
★ 「向後相容」要有定義:加可選欄位 ✅、改欄位語義 ❌、刪欄位 ❌。
-
★ 下游需求差異極大時,「每個下游一個 BFF」比一個通用協定更好(見 標準資料模型 的陷阱)。
-
還在探索邊界時不要承諾穩定協定,你會被早期錯誤綁住。
🔗 相關工具
- 發布語言 Published Language(常與 OHS 搭配,作為交換用的共享格式)
- 防腐層 Anticorruption Layer(下游側對應的自保翻譯層)
- 客戶-供應商 Customer-Supplier(一對一上下游關係的替代做法)
- 領域驅動設計
- 發布語言 —— 強調格式的姊妹模式
- 客戶-供應商 —— 為特定下游客製
- 遠端外觀 —— BFF
- 資料傳輸物件、對應器 —— 內外分離
- 標準資料模型 —— 過度通用化的陷阱
- 防腐層 —— 下游端的對應措施
- 工具-資料編碼與演進 —— 相容性規則