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¶
- 建立
cmd/twirpserver/adapters_dungeon.go、adapters_reward.go分別存放跨模組 adapter,讓 main.go 只保留 bootstrap 邏輯 - 將
dungeonGameDataAdapter、convertLubanDungeonConfig、convertLubanConditions搬進internal/modules/dungeon/ - 將
pendingStickerGameData搬進internal/modules/sticker/ - 建立獨立 issue:「調整 sticker 的跨模組 adapter 位置以符合新規則」,確保 repo-wide 一致性
- 在設計文件中明文記錄原則:「只依賴 infra/shared pkg 的 adapter 屬於 module;依賴其他 feature module 的 adapter 屬於 composition layer」