Adapter 建好之後,需求通常不會停:「這個欄位順手加一下」「那段流程也塞進來」。每一次看起來都是小事,但堆久了,這一層就會變得很難動。所以我想在一開始就講清楚,這一層該做什麼、不該做什麼。
「反正 Adapter 已經接到 ERP 了,這裡處理比較快。」
這個想法很容易理解。不過,如果訂單、庫存、報表與權限都往裡面加,之後任何需求都要改它,就會慢慢形成另一個很大的維護負擔。
所以,我會先留一份責任清單。每次加功能時,一起對照看看,這段工作是不是真的屬於 Adapter。
把事情放回負責的人手上,後面的變更才比較好安排。所以我會先把 Adapter 的工作放在這幾項:協定轉換、ERP 認證、資料格式對應、timeout、錯誤標準化、稽核 metadata,以及必要的相容性處理。
跨域流程、前端需要的資料組裝、角色判斷與平台主資料,則回到各自的服務。

圖 Day 14-1:Adapter 責任邊界。
遇到不好分的邏輯,我會先問:如果以後 ERP 換掉了,這段判斷還需要嗎?如果還需要,就值得回到業務服務檢查;如果只是為了配合這套 ERP,才比較接近 Adapter 的介接工作。這個問題可以幫助討論,再搭配責任清單確認。
也可以偶爾回頭看看最近的 commit。Adapter 多半是因為 ERP 介面調整而改,還是每個新業務需求都會動到它?如果是後者,我會再看看有沒有功能放錯地方。
萬用 execute() 雖然方便,但參數裡到底能要求哪些操作,就得再追下去看。介面寫得具體一點,審查權限、白名單與呼叫範圍時,也會比較有依據。
所以介面上,我會傾向把用途寫明白,例如 MaterialErpClient 與 OrderErpClient。每個操作有自己的 request/response DTO 與錯誤模型,看程式時比較知道它會做什麼,也比較容易測試:
// Adapter 對外的兩個 facade,依實作的 REST 端點整理;套件與 ERP 名稱已匿名化
public interface MaterialErpClient {
MaterialDataResponse getMaterialData(@NotBlank String materialNo);
List<MaterialListItemResponse> getAllMaterials(String keyword, String categoryCode, String unit);
MaterialSyncResponse syncMaterials(@Valid MaterialSyncRequest request);
}
public interface OrderErpClient {
ShipmentResponse createShipment(@Valid CreateShipmentRequest request); // 轉單,需來源出貨通知單號
ReturnResponse createReturn(@Valid CreateReturnRequest request); // 銷退單
}
// 呼叫端只拿得到這幾個操作;想「順便」做別的事,介面上沒有入口
錯誤模型則不分 facade,一律回同一種形狀:
{
"success": false,
"code": "ERP-TIMEOUT-001",
"message": "ERP 未在時限內回應",
"timestamp": "2026-09-10T08:30:00Z",
"path": "/api/erp/orders/shipments",
"traceId": "..."
}
錯誤回應也要守住範圍,ERP 原始 stack trace 與敏感欄位留在內部。即使回了 retryable,呼叫端仍要確認交易狀態與重試策略,尤其寫入操作,要先想好重送會不會重複執行。
| 取捨 | 這樣選的理由 | 何時要重新評估 |
|---|---|---|
| 依領域建立多個 facade | 每個操作用途明確,可審查、可測試 | facade 數量成長到難以維護時,檢查是否邊界切錯 |
| 不提供萬用 execute 入口 | 避免呼叫端行為無法檢查 | 有明確受控的批次或維運需求時,需另訂控制 |
| retryable 僅作為提示 | Adapter 不掌握呼叫端交易狀態 | 特定操作已具備冪等保證時 |
| 業務規則一律不進 Adapter | 保持變更來源單一 | 無;若確有例外,應以 ADR 記錄理由 |
DTO 要讓呼叫端看得懂,也要保留做決定需要的資訊。整理欄位時,就回到這個用途確認:哪些是平台需要的業務語意,哪些只是 ERP 的內部格式,分別處理。
Adapter 自己寫的測試,只能證明它照自己的定義做對了:就算欄位名稱改了,測試也跟著改,跑起來還是全部通過。所以契約改由呼叫端寫:訂單服務寫下它會送哪些欄位、期待拿回什麼,這份契約放進 Adapter 的建置流程;Adapter 改壞介面時,在自己的 CI 就知道弄壞了誰,不用等上線。這就是 consumer-driven contract test,用它驗證平台契約,再以 stub 模擬 timeout、SOAP fault、空值與格式異常:
| 驗證項目 | 做法 | 想確認什麼 |
|---|---|---|
| 契約相容性 | consumer-driven contract test | 介面變更是否破壞既有呼叫端? |
| 異常路徑 | stub 模擬 timeout、fault、空值、格式異常 | 錯誤是否都轉成標準模型? |
| 敏感資訊外洩 | 檢查對外回應與 Log 內容 | 是否夾帶 ERP stack trace 或敏感欄位? |
| 邊界守則 | 檢視 Adapter 近期變更來源 | 變更是否由 ERP 介面異動驅動? |
最後一項可以放進定期架構審查。偶爾回頭看新增了哪些責任,發現放錯地方的功能,就提早整理,之後的修改也會比較輕鬆。
今天想留下的重點,是每次新增功能,都再回來看一次責任清單。把介接與業務工作分好,Adapter 才能維持原本的用途。下一篇,再往資料、錯誤與版本的轉換細節走。