🎯 什麼情境該想到我
當你「業務邏輯被資料庫/框架/第三方 SDK 綁死,換掉就要重寫」的時候,
或「想理解為什麼 Clean Architecture 要畫那些同心圓」的時候。
⚙️ 怎麼用
兩條原則,一體兩面
① 依賴反轉原則(DIP) —— 關於依賴方向
高層模組不應依賴低層模組,兩者都應依賴抽象。抽象不應依賴細節,細節應依賴抽象。
② 好萊塢原則 —— 關於控制流方向
“Don’t call us, we’ll call you.” 由高層決定何時呼叫低層,低層只實作被回呼的介面。
★ 「反轉」到底反轉了什麼
❌ 傳統:控制流與依賴方向「一致」
OrderService ──呼叫且 import──▶ PostgresRepo
★ 業務邏輯依賴資料庫實作 → 換資料庫要改業務邏輯
✅ 反轉後:控制流不變,★ 依賴方向被反轉
OrderService ──呼叫──▶ OrderRepository(介面,★ 定義在業務層)
▲
│ 實作(依賴方向往上)
PostgresRepo(基礎設施層)
★ 關鍵:介面「屬於」高層,不屬於低層。
OrderRepository這個介面和OrderService放在同一個套件——
它表達的是「業務邏輯需要什麼」,而不是「資料庫能提供什麼」。這樣一來,基礎設施依賴業務邏輯,而不是反過來——依賴方向被「反轉」了。
這正是 Clean Architecture / 六角架構那些同心圓的全部內容。
★ Go 的長相
// ── 業務層(不 import 任何基礎設施)──
package order
// ★ 介面定義在這裡,而且只有業務邏輯真正需要的方法
type Repository interface {
Save(ctx context.Context, o *Order) error
FindByID(ctx context.Context, id string) (*Order, error)
}
type Service struct{ repo Repository } // ★ 依賴抽象
func (s *Service) Place(ctx context.Context, req Request) (*Order, error) {
o := newOrder(req) // 業務規則
if err := o.Validate(); err != nil { return nil, err }
return o, s.repo.Save(ctx, o)
}// ── 基礎設施層(import 業務層)──
package postgres
type OrderRepo struct{ db *sql.DB }
func (r *OrderRepo) Save(ctx context.Context, o *order.Order) error { ... }
// ★ 不需要寫 "implements order.Repository" —— Go 的隱式滿足// ── main:唯一知道所有具體型別的地方 ──
func main() {
db, _ := sql.Open("postgres", dsn)
svc := order.NewService(&postgres.OrderRepo{DB: db}) // ★ 組裝
}★ order 套件完全不 import postgres——
這可以用 go list -deps 或架構測試(如 go-arch-lint)自動驗證,
讓「架構規則」從文件變成 CI 檢查。
★ 依賴反轉 vs 依賴注入(常被混淆)
| 依賴反轉(DIP) | 依賴注入(DI) | |
|---|---|---|
| 是什麼 | ★ 原則:依賴方向該指向抽象 | ★ 手法:從外部把相依傳進來 |
| 回答 | 「該依賴什麼」 | 「怎麼拿到它」 |
| 沒有對方行嗎 | 可以只用 DI 不做 DIP(注入一個具體型別) | 可以只做 DIP 不用 DI(用 service locator,但不建議) |
★ 一句話:DIP 決定「依賴誰」,DI 決定「怎麼拿到」。 兩者常一起出現,但不是同一件事。
注入一個具體的*postgres.OrderRepo是 DI 但不是 DIP。
★ 好萊塢原則:函式庫 vs 框架
函式庫(library):★ 你呼叫它 → 你控制流程
result := json.Marshal(v)
框架(framework):★ 它呼叫你 → 它控制流程(控制反轉,IoC)
http.HandleFunc("/x", myHandler) // 你只是註冊,何時執行由框架決定
「控制反轉」的其他常見形態:
| 形態 | 例子 |
|---|---|
| 回呼/事件處理器 | http.Handler、EventEmitter |
| 範本方法模式 | 父類控制骨架,子類填空 |
| 觀察者模式 | Subject 決定何時通知 |
| middleware | 框架決定何時呼叫 next |
| 生命週期 hook | init()、React 的 useEffect |
🧪 我實際套用的紀錄
- 2026-07-14:(待填)
⚠️ 注意 / 什麼時候不適用
- ★ 介面放錯邊是最常見的錯誤。把
Repository介面放在postgres套件裡 →
業務邏輯還是要 import 基礎設施,完全沒有反轉。 介面必須屬於使用它的那一方
(見 工具-針對介面編程)。 - ★ 介面不要照著資料庫設計。
FindByUserIDAndStatusOrderByCreatedAt這種方法名
代表你在用 ORM 的語言描述業務需求——那是依賴沒有真正反轉的訊號。 - ★ 不是每一層都要反轉。小專案、CRUD、一次性腳本直接用具體型別更快更清楚。
DIP 的成本(多一個介面、多一層間接、追程式要跳)在簡單系統裡不划算。 - ★ 抽象洩漏:介面上出現
sql.Tx、*gorm.DB、context.Context裡塞資料庫交易 →
抽象已經洩漏了。事務邊界的處理是這裡最難的部分(常見解法是 Unit of Work,見 工具-領域邏輯組織模式)。 - DI 框架不是必需品。Go 社群多半在
main()裡手動組裝就好;
相依真的很多時才考慮wire(編譯期產生程式碼,比反射式的 DI 容器好除錯)。 - 好萊塢原則讓控制流難追。「誰呼叫了我」在框架裡不明顯——這是控制反轉的固有代價。
🔗 相關工具
- 工具-針對介面編程 —— ★ 介面該定義在哪一邊
- 工具-封裝變化點 —— 先找出該被反轉的是什麼
- 工具-優先組合而非繼承 —— 注入的前提
- 範本方法模式、觀察者模式 —— 好萊塢原則的具體模式
- 工具-服務邊界與演進式拆分 —— 依賴方向決定了邊界能不能切
- 工具-打破依賴以便測試 —— 反轉之後測試才好寫
- 工具-領域邏輯組織模式 —— 事務邊界的處理