跳轉到

Go Adapter 定位原則:infra-only 歸 module、跨模組歸 composition root

文檔資訊

  • 分類: architecture
  • 難度: intermediate
  • 預估閱讀時間: 5 分鐘
  • 標籤: go, adapter, clean-architecture, composition-root, import-direction, module-boundary, main.go-refactor

摘要

討論如何判斷 Go 專案中 adapter 的正確位置:依賴只有 infra/pkg 的 adapter 放進各自 module,依賴其他 feature module 的 adapter 放在 cmd/(composition root)。以 MatchRPG_Server 的 8 個 main.go adapter 為具體案例,得出可複用的分類標準。

關鍵學習

  • 判斷 adapter 位置的唯一標準是「import direction」:它還依賴誰?

  • 只依賴 pkg/ infra(postgres, redis, luban, S3, config)的 adapter → 歸屬 module 自己的目錄

  • 依賴其他 feature module 的 adapter → 歸屬 cmd/(composition root),因為只有這層知道完整依賴圖

  • 跨模組 adapter 放進 module 會製造 import cycle 風險,且單元測試會拉進外模組所有依賴

  • 現有 sticker 模組是反例(跨模組 adapter 放在 module 內),屬於歷史不一致,需另立 issue 清理

技術細節

核心判斷標準

adapter 的歸屬取決於 import direction,用一句話表達:

只依賴 infra/shared pkg 的 adapter 屬於 module;依賴其他 feature module 的 adapter 屬於 integration/composition layer。

為什麼跨模組 adapter 不能放進 module?

若把 dungeonPlayerAdapter 搬進 internal/modules/dungeon/,則 dungeon package 必須 import .../player,後果:

  • dungeon 的單元測試會連帶拉進 player 的所有依賴
  • 若之後有 player → dungeon 的反向呼叫,立即形成 package cycle

8 個 main.go adapter 的分類結果

# 名字 真正的依賴 分類 新位置
1 playerRewardGranter player + activity + reward + luban 跨模組 cmd/twirpserver/
2 dungeonPlayerAdapter dungeon port + player 跨模組 cmd/twirpserver/
3 dungeonRewardResolverAdapter dungeon + player.GameDataProvider + reward + luban 跨模組 cmd/twirpserver/
4 dungeonGameDataAdapter dungeon port + pkg/config + luban 模組自有 internal/modules/dungeon/
5 convertLubanDungeonConfig 只用 luban + dungeon 型別 模組自有 helper internal/modules/dungeon/
6 convertLubanConditions 只用 luban + dungeon 型別 模組自有 helper internal/modules/dungeon/
7 initS3FileStorage 讀 env → pkg/storage Bootstrap helper 留在 cmd/twirpserver/main.go
8 pendingStickerGameData 只用 sticker port 模組自有 stub internal/modules/sticker/

Sticker 不一致性

repo 現有 sticker 模組是反例——跨模組 adapter 放在模組內且在 wire.go 內組裝:

internal/modules/sticker/sticker_activity_adapter.go
internal/modules/sticker/sticker_player_adapter.go
internal/modules/sticker/sticker_reward_adapter.go

這不是「語言層面的唯一標準」,而是「更嚴格的架構規則」。若要套用 repo-wide,需另立 PR 清理 sticker。

What Changed

這次確立的架構判斷原則

原本以為所有 adapter 應該全部搬到 cmd/twirpserver/,但經過 import dependency 分析後,修正為:dungeonGameDataAdapter(+2 個 helper,共 ~172 行)以及 pendingStickerGameData(~28 行)應搬進各自 module,而不是留在 composition root。

具體範圍決策

這次只做 main.go 瘦身,sticker 現有的跨模組 adapter 不動(它們已跑通、測試過),另立獨立 issue/PR 處理規則對齊。

So What

這個原則可以在未來所有模組新增 adapter 時直接套用,避免反覆討論「adapter 應該放哪」。

更重要的是,import direction 這個判斷標準比「放在 cmd/ 還是 module/」更底層,能在 import cycle 發生前就做出正確決策,是可複用的架構思維框架。

Trade-offs

  • 跨模組 adapter 留在 cmd/:優點是 module 保持純粹、無 cycle 風險;缺點是 cmd/ 可能隨業務增長膨脹,需靠分檔(adapters_dungeon.go 等)管理
  • module-only adapter 搬進 module:優點是模組自包含、便於單獨測試;缺點是初期需要額外搬移工作
  • 不動 sticker:優點是不影響現有通過的測試;缺點是 repo 存在兩套規則,新人容易混淆

Try It Fast

# 確認 dungeonGameDataAdapter 確實只依賴 infra,沒有 import 其他 feature module
grep -n 'import' /path/to/main.go | head -40

# 搬移後驗證無 import cycle
go build ./...

# 確認 sticker 現有跨模組 adapter(未動)
cat internal/modules/sticker/sticker_player_adapter.go | head -15

Recommendation

  1. 建立 cmd/twirpserver/adapters_dungeon.goadapters_reward.go 分別存放跨模組 adapter,讓 main.go 只保留 bootstrap 邏輯
  2. dungeonGameDataAdapterconvertLubanDungeonConfigconvertLubanConditions 搬進 internal/modules/dungeon/
  3. pendingStickerGameData 搬進 internal/modules/sticker/
  4. 建立獨立 issue:「調整 sticker 的跨模組 adapter 位置以符合新規則」,確保 repo-wide 一致性
  5. 在設計文件中明文記錄原則:「只依賴 infra/shared pkg 的 adapter 屬於 module;依賴其他 feature module 的 adapter 屬於 composition layer」

本文檔由 Semi-Brain 自動生成

Session ID: c014ee9c-4558-46a6-9a87-1c574390eeec

分析信心度: 88%