🎯 什麼情境該想到我

當你「多個上下文/系統之間要交換資料,卻各用各的內部格式,兩兩翻譯很混亂」的時候。你想約定一套公開、有良好文件的共享語言/格式,作為大家交換的共同媒介。

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

意圖:採用一套公開、文件完善的共享語言(常是標準化的 schema/格式,如產業標準或明確定義的交換格式)作為系統間資料交換的共同媒介,讓各方都翻譯到/自這套語言。

做法要點:

  1. 選用或制定一套良好文件化的公開交換格式(產業標準、或雙方共同定義的 schema)。
  2. 每個上下文只需在自己與「發布語言」之間做一次翻譯,而非跟每個對象各自對接。
  3. 明確版本化與文件化這套語言,讓所有參與方有共同依據。
  4. 常與開放主機服務搭配: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 為準」那不是發布語言。

  • ★ 未知欄位的處理規則要明文規定(忽略 = 可演進;報錯 = 無法加欄位)。

  • ★ 誰能改、破壞性變更的公告期要寫下來,否則退化成「誰先改誰贏」。

🔗 相關工具