🎯 什麼情境該想到我
當你「多個上下文/系統之間要交換資料,卻各用各的內部格式,兩兩翻譯很混亂」的時候。你想約定一套公開、有良好文件的共享語言/格式,作為大家交換的共同媒介。
⚙️ 怎麼用(步驟 / 公式)
意圖:採用一套公開、文件完善的共享語言(常是標準化的 schema/格式,如產業標準或明確定義的交換格式)作為系統間資料交換的共同媒介,讓各方都翻譯到/自這套語言。
做法要點:
- 選用或制定一套良好文件化的公開交換格式(產業標準、或雙方共同定義的 schema)。
- 每個上下文只需在自己與「發布語言」之間做一次翻譯,而非跟每個對象各自對接。
- 明確版本化與文件化這套語言,讓所有參與方有共同依據。
- 常與開放主機服務搭配:OHS 是對外的服務協定,發布語言是它交換的資料語彙。
★★ 它和「開放主機服務」的分工:格式 vs 服務
★ [[開放主機服務]]:★★ 「我提供一組穩定的操作」(★ 服務與協定)
★ 發布語言: ★★ 「我們用這個格式交換資料」(★ 純資料格式,可能沒有服務)
→ ★ 兩者常一起出現,★★ 但發布語言可以獨立存在:
★ 一份 CSV 規格、一個 Kafka topic 的 Avro schema、一個 webhook payload 格式
★★ 發布語言最純粹的形態是「一份有版本的 schema,而沒有任何服務」——
★ 例如你發到 Kafka 的事件格式(見 事件訊息)。
★★ 產業標準的發布語言:能用就不要自己發明
★ 各領域都有現成的:
★★ 金融:ISO 20022、FIX ★★ 醫療:FHIR、HL7
★ 電商:GS1、schema.org ★ 發票/稅務:各國的電子發票規格
★★ 可觀測性:OpenTelemetry ★ 事件:CloudEvents
★ 身分:OIDC 的 claims
→ ★★ 用它們的三個好處:
① ★ 治理不是你的問題(★ 規格由別人維護)
② ★★ 已經被很多人壓力測試過(★ 邊界情況都踩過了)
③ ★ 下游可能已經支援了 ⇒ ★★ 整合成本趨近於零
★★ 判準:這個領域有產業標準嗎? 有 → ★ 用它,即使它比你設計的醜。
★ 「醜但通用」勝過「漂亮但只有你懂」(見 追隨者 的同一論點)。
★★ 自己定義時:五個必須先決定的事
① ★★ schema 的「真相」放哪裡
✅ ★ 一個獨立的 repo/目錄,★ 由它產生各語言的型別
❌ ★★ 「以 Go struct 為準,其他語言自己對照」→ ★ 那不是發布語言,是你的內部型別
② ★★ 版本策略(★ 必須在第一版就決定)
★ 語義化版本?★ 只做加法?★★ 不相容時開新的 topic/endpoint?
→ ★ 見 [[格式指示器]]:★★ 「通道名帶大版本 + 訊息帶小版本」是最穩的組合
③ ★★ 相容性規則要寫成文字
★ 「可以加可選欄位」「不可以改欄位語義」「不可以重用 protobuf 欄號」
→ ★★ 而且要能被機器檢查(buf breaking、protolock、json-schema diff)
④ ★★ 誰能改、怎麼改
★ CODEOWNERS + ★ 「破壞性變更需要 N 天公告期」
→ ★ 沒有這個,★★ 發布語言會退化成「誰先改誰贏」
⑤ ★ 未知欄位的處理規則
★★ 消費者遇到不認識的欄位要「忽略」還是「報錯」?
→ ★ 必須明文規定(★ 忽略 = 可演進;報錯 = 嚴格但無法加欄位)
★★ ③ 的「能被機器檢查」是關鍵:
★ 相容性規則寫在 wiki 上等於沒有規則。
→ ★ buf breaking 對 protobuf、★ JSON Schema 的 diff 工具對 JSON,
★★ 讓破壞性變更在 PR 階段就紅燈。
★★ Go 的落地:schema 是真相,Go 型別由它產生
schemas/ ★★ 獨立目錄或獨立 repo
events/
order_placed_v1.proto
order_placed_v2.proto ★ 不相容變更 → 新檔案、新 topic
api/
orders_v1.yaml ★ OpenAPI
# ★ CI 的三道檢查
buf lint # ★ 風格
buf breaking --against '.git#branch=main' # ★★ 破壞性變更檢查
protoc-gen-go … && git diff --exit-code # ★★ 產生的碼與 repo 一致★★ 第三道檢查是最容易被忽略、也最常出事的:
★ 有人改了 .proto 但忘記 regenerate →
編譯過了(因為舊的 .pb.go 還在),但實際發出的格式和 schema 不一致。
★★ 一個常被忽略的問題:發布語言的「語義」無法被 schema 表達
★ schema 能表達:★ 欄位名、型別、必填與否、列舉值
★★ schema 不能表達:
★ 「amount 是以最小單位計」(★ 見 [[金錢]]:TWD 是元、USD 是分、BHD 是千分)
★ 「occurred_at 是事件發生時間,不是送出時間」
★★ 「status 從 pending 只能轉到 paid 或 cancelled」
★ 「這個欄位在 v1.3 之後語義改了」(★★ 而 schema 看起來沒變)
→ ★ 所以發布語言必須配一份「語義文件」,★★ 而且它要和 schema 放在一起
★★ 最後一項最危險:★ 「欄位名沒變、型別沒變,但意義變了」
——任何機器檢查都抓不到,而下游會靜默地算錯。
→ ★ 對策:★★ 語義變更一律視為破壞性變更,開新版本。
★ 兄弟辨析
| 差別 | |
|---|---|
| 開放主機服務 | ★ 強調服務與操作;發布語言強調格式 |
| 共享核心 | ★★ 共用程式碼(耦合重得多);發布語言只共用格式 |
| 格式指示器 | ★ 版本怎麼標在訊息裡 |
| 標準資料模型 | ★ 過度通用化的陷阱 |
| 正規化器 | ★ 多種外部格式歸一成你的發布語言 |
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
制定與維護一套共享語言有成本;只有兩方、且關係簡單時,直接翻譯可能更省。
-
公開格式一旦被多方採用就難改,演化受限,需要嚴謹的版本治理。
-
通用格式常無法完美貼合每個上下文,翻譯時會有語意落差要留意。
-
★★ 這個領域有產業標準嗎?有 → 用它,即使它比你設計的醜。 「醜但通用」勝過「漂亮但只有你懂」。
-
★★ 相容性規則必須能被機器檢查(
buf breaking、JSON Schema diff),寫在 wiki 上等於沒有規則。 -
★★ CI 要檢查「產生的碼與 repo 一致」。 改了
.proto忘記 regenerate → 編譯過了但實際格式與 schema 不一致。 -
★★ schema 表達不了語義(amount 的最小單位、occurred_at 是發生時間、狀態機的合法轉移)。必須配一份語義文件,和 schema 放在一起。
-
★★ 「欄位名沒變、型別沒變,但意義變了」任何機器檢查都抓不到,下游會靜默算錯。 語義變更一律視為破壞性變更。
-
★ schema 的真相要在獨立目錄/repo,由它產生各語言型別。「以 Go struct 為準」那不是發布語言。
-
★ 未知欄位的處理規則要明文規定(忽略 = 可演進;報錯 = 無法加欄位)。
-
★ 誰能改、破壞性變更的公告期要寫下來,否則退化成「誰先改誰贏」。
🔗 相關工具
- 開放主機服務 Open Host Service(最常搭配的上游模式)
- 防腐層 Anticorruption Layer(各方把發布語言翻譯進內部模型的手段)
- 上下文對應 Context Map(發布語言是地圖上一種整合關係)
- 領域驅動設計
- 開放主機服務 —— 強調服務的姊妹模式
- 共享核心 —— 共用程式碼(耦合重得多)
- 格式指示器 —— 版本怎麼標
- 事件訊息 —— 最常見的發布語言載體
- 標準資料模型 —— 過度通用化的陷阱
- 正規化器 —— 多來源歸一
- 金錢 —— schema 表達不了的語義範例
- 工具-資料編碼與演進 —— 相容性規則