🎯 什麼情境該想到我

當你「要整合一個外部系統、遺留系統或第三方 API,但不想讓它那套(可能很糟或跟你格格不入的)模型滲進來污染你乾淨的領域模型」的時候。你想在中間加一層翻譯把它擋在外面。

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

意圖:在下游與上游之間建立一層隔離+翻譯層,把上游的模型轉換成下游自己的模型。上游的概念與變動被擋在這層之外,保護內部模型的純淨與穩定。

做法要點:

  1. 定義你自己的內部模型與介面(你「想要」的樣子),與上游脫鉤。
  2. 在邊界建立翻譯層,將上游的資料/概念雙向轉換成內部模型(常用 Facade/Adapter,必要時搭配 Service)。
  3. 內部程式只跟自己的模型與這層介面打交道,絕不直接依賴上游型別。
  4. 上游一變,只改翻譯層這一處,內部模型不受波及。

★★ 它的價值只有一句:讓上游的變化止步於一個檔案

★ 沒有防腐層:★ Stripe 改版 → ★★ 三十個檔案要改
★ 有防腐層:  ★ Stripe 改版 → ★★ 一個 package 要改,★ 領域層一行都不動
→ ★ 這個差異在「上游真的改了」那一刻一次兌現,★★ 而在那之前它只是成本

★★ 所以防腐層是一個「保險」:★ 你付固定的保費(翻譯層的維護),
換取「上游變化不會擴散」的保障。
★ 判準:這個上游「多久會變一次」以及「變了你會痛多久」?

★★ 它不只是「包一層 API」——四件必須翻譯的事

① ★★ 型別與結構
   ★ stripe.Charge(幾十個欄位)→ ★ 你的 Payment(★ 只有你在意的五個)

② ★★ 錯誤語義(★ 最常被跳過,也最重要)
   ★ stripe.Error{Code: "card_declined"} → ★ ErrCardDeclined
   ★ stripe.Error{Code: "rate_limit"}    → ★★ ErrTemporary(★ 可重試)
   → ★★ 沒有這一步,★ 你的重試邏輯必須認識 Stripe 的錯誤碼 ⇒ 防腐層形同虛設

③ ★★ 詞彙(★ 這是「防腐」真正的意思)
   ★ 上游說 "charge"、"capture"、"void"
   ★ 你的領域說「收款」「請款」「作廢」
   → ★★ 若你的領域碼裡出現 Capture(),★ 你已經被上游的語言同化了
     (★ 那就是 [[追隨者]],而不是防腐層)

④ ★ 一致性模型
   ★ 上游是最終一致(webhook 稍後才到)→ ★★ 你的領域要能表達「等待中」
   → ★ 這不是翻譯,是★★ 建模:★ 你需要一個上游沒有的狀態

★★ ③ 是判斷「這真的是防腐層嗎」的試紙:
★ 打開你的領域 package,★★ 如果詞彙是你自己的 → 是防腐層;
如果詞彙是上游的 → 你只是包了一層,本質上仍是追隨者。

★★ Go 的實作:三層結構

// ★ ① 領域層宣告它需要什麼(★ 用你的詞彙)
package payment
type Gateway interface {
    Collect(ctx context.Context, amount Money, method Method) (Receipt, error)
    Reverse(ctx context.Context, r ReceiptID, amount Money) error
}
var (
    ErrDeclined  = errors.New("付款被拒絕")     // ★ 你的錯誤
    ErrTemporary = errors.New("暫時性失敗")      // ★ 可重試
)
 
// ★★ ② 防腐層:★ 唯一認識上游的地方
package stripegw
type Gateway struct{ c *stripe.Client }
 
func (g *Gateway) Collect(ctx context.Context, amount payment.Money, m payment.Method) (payment.Receipt, error) {
    ch, err := g.c.Charges.New(toStripeParams(amount, m))     // ★ 型別翻譯
    if err != nil { return payment.Receipt{}, translateErr(err) }  // ★★ 錯誤翻譯
    return toReceipt(ch), nil                                  // ★ 詞彙翻譯
}
 
// ★ ③ CI 守住邊界
// depguard: internal/payment 不得 import github.com/stripe/**

★★ translateErr 是這個模式的心臟,而它是最常被寫成一行 return err 的地方。

★★ 防腐層的三個實務代價(誠實列出)

① ★★ 你會失去上游的部分能力
   ★ Stripe 有 presigned checkout、有 Radar 風控、有 30 種支付方式
   → ★ 你的 Gateway 介面只暴露五個方法 ⇒ ★★ 用不到那些
   → ★ 對策:★★ 需要時就加方法(★ 而不是一開始就設計一個「完整」的介面)

② ★★ 雙重維護
   ★ 上游加一個欄位而你需要它 → ★ 要改三個地方(介面、翻譯、領域型別)
   → ★ 這是固定成本,★★ 而它只在「上游頻繁加你需要的東西」時才痛

③ ★★ 它可能被繞過
   ★ 「這次趕時間,直接用 stripe 的 SDK」→ ★★ 而「這次」會變成常態
   → ★ 對策:★★ depguard,讓繞過變成 CI 紅燈(★ 這是唯一有效的機制)

★★ 什麼時候「不要」防腐層

① ★★ 上游是穩定的產業標準且你的領域沒有獨特規則
   → ★ [[追隨者]] 更便宜(★ 見該頁的「中間點」做法)
② ★ 唯讀的參照資料(匯率表、地址庫)
   → ★★ 沒有領域邏輯要保護
③ ★ 一次性/短期整合
④ ★★ 你其實不該整合
   → ★ 先問 [[各行其道]]

★★ 最實用的中間形態:★ 「薄防腐層」——
★★ 只翻譯錯誤與領域邏輯會判斷的欄位,其餘直接用上游的型別。
★ 它拿到 80% 的保護,成本只有 20%(見 追隨者 的同一段)。

★ 兄弟辨析

差別
追隨者★★ 不翻譯,接受模型被形塑
閘道★ PoEAA 的說法,★ 範圍較窄(封裝資源存取)
對應器★ 只做型別轉換,★★ 不含錯誤與詞彙的翻譯
開放主機服務★ 上游主動提供的穩定協定(★ 那時你不需要防腐層)
正規化器★ 多個來源歸一到一種格式

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • 翻譯層要花額外開發與維護成本;若上游模型本來就乾淨又契合,追隨者可能更划算。

  • 這是防禦性投資,回報在「上游多變或很糟」時才明顯;上游穩定良好時可能過度設計。

  • 翻譯邏輯若做得太厚重,本身也可能變成難維護的一坨,要控制它的複雜度。

  • ★★ 「錯誤語義的翻譯」最常被跳過,而它決定了防腐層是不是真的有效。 沒有它,你的重試邏輯必須認識上游的錯誤碼。

  • ★★ 判斷試紙:打開領域 package,詞彙是你自己的還是上游的? 是上游的 → 你只是包了一層,本質上仍是 追隨者。

  • ★★ 它可能被繞過(「這次趕時間」會變成常態)。 depguard 是唯一有效的機制。

  • ★★ 最實用的形態是「薄防腐層」:只翻譯錯誤與領域邏輯會判斷的欄位,其餘直接用上游型別。80% 保護、20% 成本。

  • ★ 你會失去上游的部分能力(風控、多種支付方式)。對策是「需要時再加方法」,不是一開始設計一個完整介面。

  • ★ 上游的一致性模型不同時,你需要一個上游沒有的狀態(「等待中」)——那不是翻譯,是建模。

  • ★ 它是一個保險:付固定保費,在「上游真的改了」那一刻一次兌現。判準是「上游多久變一次、變了你痛多久」。

  • 唯讀參照資料與一次性整合不需要防腐層。

🔗 相關工具