← 回 knowledge index

Knowledge Mirror

依賴注入原理、實作與設計模式:Pure DI、Container、生命週期與設計模式

來源筆記:依賴注入原理、實作與設計模式:Pure DI、Container、生命週期與設計模式.md

草稿整理:依據目前關於 Dependency Injection(DI)、Pure DI、DI Container、Service Locator、生命週期管理、Composition Root 與設計模式的討論整理。 依賴注入的本質,是把「使用物件」跟「建立物件」分離。 物件本身不應該自己決定要建立哪個具體依賴,而是宣告自己需要什麼,由外部在組裝階段把依賴交給它。…

依賴注入原理、實作與設計模式:Pure DI、Container、生命週期與設計模式

草稿整理:依據目前關於 Dependency Injection(DI)、Pure DI、DI Container、Service Locator、生命週期管理、Composition Root 與設計模式的討論整理。

一句話總結

依賴注入的本質,是把「使用物件」跟「建立物件」分離。

物件本身不應該自己決定要建立哪個具體依賴,而是宣告自己需要什麼,由外部在組裝階段把依賴交給它。

// 不推薦:OrderService 自己建立依賴
class OrderService {
  private repo = new OrderRepository()
  private payment = new StripePaymentGateway()
}

// 推薦:依賴由外部注入
class OrderService {
  constructor(
    private repo: OrderRepository,
    private payment: PaymentGateway
  ) {}
}

DI 的價值不是少寫幾個 new,而是讓系統更容易:

DI、IoC 與依賴反轉

DI 常和 IoC 一起出現。

IoC(Inversion of Control,控制反轉)是一種設計思想:原本由物件自己控制依賴建立,改成由外部控制。

DI 是實作 IoC 的一種方式。

原本:物件自己 new 依賴
改成:外部建立依賴後注入物件

這也呼應依賴反轉原則:

高層業務邏輯不應依賴低層具體實作,兩者都應依賴抽象。

例如:

interface PaymentGateway {
  charge(amount: number): Promise<void>
}

class OrderService {
  constructor(private payment: PaymentGateway) {}
}

OrderService 不需要知道背後是 Stripe、PayPal,還是測試用 FakePaymentGateway。

IoC、DI 與依賴反轉的差異

這三個概念常被混在一起,但它們其實位在不同層次:

IoC:控制權反轉的泛稱 / 思想
DI:實作 IoC 的一種手法,專門處理依賴取得
DIP:Dependency Inversion Principle,依賴反轉原則,SOLID 裡的設計原則

IoC:誰控制流程或建立?

IoC(Inversion of Control,控制反轉)指的是原本由自己的程式主動控制流程、建立物件或呼叫邏輯,改成由外部框架、容器或環境來控制。

例如傳統程式:

const app = new App()
app.run()

這是你的程式主動控制流程。

Web framework 裡常見的寫法:

router.get('/users', userController.list)

你只是註冊 handler,真正什麼時候呼叫它,是 framework 決定。這就是 IoC。

IoC 可以出現在很多地方,不一定跟 DI 有關:

DI:依賴從哪裡來?

DI(Dependency Injection,依賴注入)是 IoC 的一種具體形式,專門處理「依賴怎麼取得」。

原本物件自己建立依賴:

class OrderService {
  private payment = new StripePaymentGateway()
}

改成外部注入:

class OrderService {
  constructor(private payment: PaymentGateway) {}
}

控制權從 OrderService 內部轉到外部組裝者。

所以:

DI 一定是某種 IoC。
但 IoC 不一定是 DI。

DIP:程式碼依賴抽象還是具體實作?

依賴反轉通常指 DIP(Dependency Inversion Principle,依賴反轉原則)

它不是工具,也不是注入方式,而是一條設計原則:

高層模組不應依賴低層模組;兩者都應依賴抽象。
抽象不應依賴細節;細節應依賴抽象。

壞例子:

class OrderService {
  private payment = new StripePaymentGateway()
}

OrderService 是高層業務邏輯,卻直接依賴低層 Stripe 實作。

比較好的版本:

interface PaymentGateway {
  charge(amount: number): Promise<void>
}

class OrderService {
  constructor(private payment: PaymentGateway) {}
}

class StripePaymentGateway implements PaymentGateway {
  charge(amount: number) {
    // call Stripe API
  }
}

這裡:

OrderService 依賴 PaymentGateway 抽象
StripePaymentGateway 也依賴 PaymentGateway 抽象

這才是依賴反轉。

有 IoC 就算有 DI 嗎?

不一定。

例如:

button.onClick(() => {
  console.log('clicked')
})

這是 IoC,因為不是你主動呼叫 callback,而是 UI framework 在事件發生時呼叫你。

但這不是 DI,因為它沒有處理物件依賴注入。

有 IoC ≠ 有 DI

有 IoC 就算有依賴反轉嗎?

也不一定。

例如:

router.get('/pay', () => {
  const stripe = new StripePaymentGateway()
  stripe.charge(...)
})

路由 handler 被 framework 呼叫,所以有 IoC。

但 handler 裡還是直接依賴 Stripe 具體實作,所以沒有做好 DIP。

有 IoC ≠ 有 DIP

有 DI 就算有依賴反轉嗎?

也不一定。

你可以把具體類別注入進去:

class OrderService {
  constructor(private payment: StripePaymentGateway) {}
}

這是 DI,因為依賴從外部注入。

但它仍然依賴具體 Stripe 類別,不是依賴抽象。

比較符合 DIP 的版本是:

class OrderService {
  constructor(private payment: PaymentGateway) {}
}

所以:

有 DI ≠ 一定有 DIP

DI 是「怎麼給依賴」。

DIP 是「應該依賴誰」。

有 DIP 一定要 DI 嗎?

嚴格說也不一定,但實務上常搭配。

可以用 Factory、Plugin、Event 等方式達成依賴抽象,不一定非得 constructor injection。

但在大多數業務程式中:

DIP 通常靠 DI 實作起來最自然。

關係圖

IoC:控制權交給外部
 └─ DI:依賴取得的控制權交給外部

DIP:高層與低層都依賴抽象

更精準地說:

IoC 是大概念
DI 是技術手法
DIP 是設計原則

一句話記法:

IoC 問:誰控制流程或建立?
DI 問:依賴從哪裡來?
DIP 問:程式碼依賴抽象還是具體實作?

結論:

有 IoC,不一定有 DI。
有 IoC,不一定有依賴反轉。
有 DI,也不一定有依賴反轉。
但好的 DI 設計,通常會用來實現依賴反轉。

Pure DI 是什麼?

Pure DI 指的是不用 DI Container,而是直接用程式碼手動組裝物件圖。

const db = new PostgresDatabase()
const userRepo = new UserRepository(db)
const email = new SendGridEmailService()
const userService = new UserService(userRepo, email)
const controller = new UserController(userService)

這仍然是 DI,因為依賴是從外部注入的;只是組裝方式是手寫,而不是交給容器。

Pure DI 優點

Pure DI 缺點

當依賴圖變大時,手動接線會變得冗長:

const a = new A()
const b = new B(a)
const c = new C(a, b)
const d = new D(c)
const e = new E(b, d)

當專案開始有大量模組、request scope、transaction scope、provider override 時,Pure DI 的組裝成本會上升。

DI Container 是什麼?

DI Container 是幫你建立物件、解析依賴、管理生命週期的工具。

例如 NestJS、Spring、Angular 都有 DI Container。

@Injectable()
class UserService {
  constructor(private repo: UserRepository) {}
}

你不需要自己寫:

new UserService(new UserRepository(...))

容器會根據註冊規則或 metadata 自動組裝。

DI Container 通常負責:

Pure DI vs DI Container

Pure DI:依賴怎麼組裝,你看得見。
DI Container:依賴怎麼組裝,交給容器處理。

什麼時候用 Pure DI?

適合:

什麼時候用 DI Container?

適合:

實務建議:

核心邏輯用 Pure DI 思維設計。
外層 framework / infrastructure 可以用 DI Container 組裝。

核心業務類別不要知道 container 存在。

Service Locator 跟 DI Container 的差別

差別在於:誰主動去拿依賴。

DI Container 正常用法

物件只宣告自己需要什麼,由外部注入。

class UserService {
  constructor(private repo: UserRepository) {}
}

UserService 不知道 container 存在。

Service Locator

物件自己去 locator / container 裡查詢依賴。

class UserService {
  createUser() {
    const repo = ServiceLocator.get(UserRepository)
    repo.save(...)
  }
}

或:

class UserService {
  constructor(private container: Container) {}

  createUser() {
    const repo = this.container.get(UserRepository)
  }
}

這就是 Service Locator。

DI Container:依賴從外面被注入進來
UserService ← repo

Service Locator:物件自己去查詢依賴
UserService → locator.get(repo)

Service Locator 常被視為反模式,因為它會把依賴藏起來:

少數可以接受 Service Locator 味道的地方:

但不要放進核心業務邏輯。

Composition Root 是什麼?

Composition Root 是整個程式裡集中建立物件、接好依賴關係的地方。

通常位於:

main.ts
app.ts
bootstrap.ts
server.ts
program.cs
Application.java

它負責:

Pure DI 的 Composition Root 可能長這樣:

const db = new Database()
const repo = new OrderRepository(db)
const payment = new StripePaymentGateway()
const orderService = new OrderService(repo, payment)
const controller = new OrderController(orderService)

DI Container 的 Composition Root 則可能是:

container.register(OrderRepository)
container.register(OrderService)
container.register(OrderController)

const app = container.resolve(App)

核心原則:

業務類別:我需要什麼。
Composition Root:我要給你什麼。

Composition Root 是組裝區,不是業務邏輯區。

副作用邊界是什麼?

副作用邊界是程式從「單純計算」走出去,開始碰到外部世界或可變狀態的地方。

純計算:

function calculateTotal(price: number, qty: number) {
  return price * qty
}

同樣輸入永遠得到同樣輸出,不碰外部世界。

副作用:

await db.save(order)
await email.send(...)
await payment.charge(...)
const now = new Date()
const id = crypto.randomUUID()

常見副作用邊界:

這些地方容易慢、失敗、不穩定、測試困難,所以特別適合用 DI 包起來。

正式環境:

const payment = new StripePaymentGateway()
const email = new SendGridEmailService()
const clock = new SystemClock()

測試環境:

const payment = new FakePaymentGateway()
const email = new FakeEmailService()
const clock = new FixedClock(new Date('2026-01-01'))

業務邏輯不用改。

生命週期管理

DI 裡的生命週期管理,是回答:

一個依賴物件應該什麼時候被建立?建立幾份?什麼時候釋放?

常見生命週期:

Singleton:整個應用程式共用同一個實例
Scoped:某個範圍內共用同一個實例
Transient:每次需要都建立新的實例

Singleton

整個 application 只有一份。

適合:

不適合放 request-specific mutable state。

例如 CurrentUser 不應該是 singleton,否則不同 request 可能共用到錯誤使用者狀態。

Scoped

在某個範圍內共用一份,最常見是 request scope。

適合:

Request A
  ├─ CurrentUser #A
  ├─ Transaction #A
  └─ RequestLogger #A

Request B
  ├─ CurrentUser #B
  ├─ Transaction #B
  └─ RequestLogger #B

Transient

每次需要就建立新的。

適合:

常見生命週期錯誤:Captive Dependency

Captive Dependency 是指長生命週期物件持有短生命週期依賴。

錯誤例子:

class SingletonOrderService {
  constructor(private currentUser: CurrentUser) {}
}

如果 OrderService 是 singleton,而 CurrentUser 是 request scoped,singleton 可能抓住第一次 request 的 user,導致後續 request 用錯狀態。

危險方向:

Singleton -> Scoped / stateful Transient

比較安全的方向:

Scoped -> Singleton
Transient -> Singleton

簡單說:短生命週期可以依賴長生命週期,但長生命週期不要直接持有短生命週期狀態。

DI 與設計模式

DI 不取代設計模式,而是讓很多設計模式更容易落地。

設計模式:定義物件之間怎麼合作。
DI:負責把這些物件接起來。

Strategy Pattern:策略模式

策略模式讓演算法或行為可替換。

interface DiscountStrategy {
  calculate(price: number): number
}

class VipDiscount implements DiscountStrategy {
  calculate(price: number) {
    return price * 0.8
  }
}

class NormalDiscount implements DiscountStrategy {
  calculate(price: number) {
    return price
  }
}

class CheckoutService {
  constructor(private discount: DiscountStrategy) {}
}

DI 負責決定注入哪個策略。

適合:付款策略、折扣策略、排序策略、風控策略、通知渠道、推薦演算法。

Adapter Pattern:轉接器模式

Adapter 把外部 API 包成系統內部想要的介面。

interface PaymentGateway {
  charge(amount: number): Promise<void>
}

class StripePaymentAdapter implements PaymentGateway {
  async charge(amount: number) {
    await stripe.paymentIntents.create({
      amount,
      currency: 'usd'
    })
  }
}

業務服務只依賴 PaymentGateway,不依賴 Stripe SDK。

這是 DI 在實務上最常見、最有價值的用法之一。

Decorator Pattern:裝飾器模式

Decorator 在不改原本類別的情況下增加行為。

interface UserRepository {
  findById(id: string): Promise<User>
}

class SqlUserRepository implements UserRepository {
  async findById(id: string) {
    return db.query(...)
  }
}

class CachedUserRepository implements UserRepository {
  constructor(private inner: UserRepository) {}

  async findById(id: string) {
    const cached = await cache.get(id)
    if (cached) return cached

    const user = await this.inner.findById(id)
    await cache.set(id, user)
    return user
  }
}

class LoggingUserRepository implements UserRepository {
  constructor(private inner: UserRepository) {}

  async findById(id: string) {
    logger.info('find user', id)
    return this.inner.findById(id)
  }
}

組合:

const repo =
  new LoggingUserRepository(
    new CachedUserRepository(
      new SqlUserRepository()
    )
  )

適合:logging、metrics、tracing、caching、retry、transaction、authorization、rate limit。

Abstract Factory:抽象工廠

有些依賴無法在 app 啟動時決定,而是 runtime 才知道。

interface PaymentGatewayFactory {
  create(method: PaymentMethod): PaymentGateway
}

class CheckoutService {
  constructor(private factory: PaymentGatewayFactory) {}

  checkout(order: Order) {
    const gateway = this.factory.create(order.paymentMethod)
    return gateway.charge(order.amount)
  }
}

需要動態建立物件時,優先注入明確 factory,而不是把整個 container 注入進業務服務。

Composite Pattern:組合模式

Composite 把多個實作包成一個實作。

interface Notifier {
  send(message: string): Promise<void>
}

class CompositeNotifier implements Notifier {
  constructor(private notifiers: Notifier[]) {}

  async send(message: string) {
    for (const notifier of this.notifiers) {
      await notifier.send(message)
    }
  }
}

業務服務只依賴一個 Notifier,不需要知道背後有 Email、Slack、SMS 幾種通知渠道。

Chain of Responsibility:責任鏈

常見於 middleware、validator、handler pipeline。

interface OrderValidator {
  validate(order: Order): void
}

class OrderValidationPipeline {
  constructor(private validators: OrderValidator[]) {}

  validate(order: Order) {
    for (const validator of this.validators) {
      validator.validate(order)
    }
  }
}

適合:middleware、validation pipeline、event handlers、command handlers、rule engine、filters。

如何不過度工程化?

核心原則:

先讓程式碼清楚,再讓它可抽換;不要為了「可能未來會換」先抽一堆介面。

1. 不要每個 class 都配一個 interface

不一定要這樣:

interface IUserService {}
class UserService implements IUserService {}

如果目前只有一個實作,而且沒有明確替換需求,直接用 concrete class 就好。

值得抽 interface 的情況:

2. 純計算與小 helper 不要硬 DI

function calculateDiscount(price: number, level: UserLevel) {
  return ...
}

這種純函式通常直接寫最乾淨,不需要硬包成 DiscountCalculator

3. 抽象要來自痛點,不要來自想像

先問:

現在有第二個實作嗎?
測試真的需要替換嗎?
這是外部服務或副作用邊界嗎?
這個依賴是否跨越架構邊界?

如果答案都是否,就先不要抽。

4. 副作用邊界用 DI,核心邏輯少抽象

最值得 DI 的地方:

DB / HTTP API / 金流 / Email / Queue / Cache / File system / Clock / UUID / Logger / Config

最不需要重 DI 的地方:

純計算 / 資料轉換 / 小 helper / value object / 簡單 validator

5. 先 Pure DI,痛了再 Container

只有一個實作、無副作用、小專案
→ 直接 new / concrete class

有副作用、需要測試替換
→ constructor injection

有多個實作,需要 runtime 切換
→ Strategy / Factory + DI

依賴圖很大,scope 很複雜
→ DI Container

核心 domain logic
→ 優先簡單、純函式、明確資料流

實務設計準則

  1. 業務核心只依賴明確依賴,不依賴 container。
class OrderService {
  constructor(
    private repo: OrderRepository,
    private payment: PaymentGateway
  ) {}
}

不要:

class OrderService {
  constructor(private container: Container) {}
}
  1. 生命週期由外層組裝層決定。

業務類別不要自己決定要 new 哪個具體依賴。

  1. 長生命週期不要抓短生命週期。

尤其 singleton 不要持有 CurrentUserRequestContextTransaction

  1. 外部系統用 Adapter 包起來。

業務邏輯不要直接依賴 Stripe SDK、SendGrid SDK、資料庫 driver。

  1. 橫切需求用 Decorator / Pipeline。

例如 logging、cache、retry、metrics、tracing。

  1. 動態選擇用 Strategy / Factory。

不要把 container 當 Service Locator 塞進業務邏輯。

最後整理

DI 的主軸不是工具,而是設計邊界:

核心業務:
  Constructor Injection + 明確依賴 + 少魔法

外部基礎設施:
  Adapter 包外部服務

橫切需求:
  Decorator 加 logging/cache/retry/metrics

多實作集合:
  Composite / Chain of Responsibility

動態選擇:
  Strategy / Abstract Factory

大型 app 組裝:
  DI Container 管生命週期與依賴圖

一句話:

DI 不是只為了少寫 new,而是為了讓物件的建立、替換、組合、生命週期,從業務邏輯裡分離出去。