問題背景

class OrderService:
    def __init__(self):
        self.repo = PostgresOrderRepository()  # 直接 new 依賴
        self.mailer = GmailMailer()            # 直接 new 依賴
 
    def create_order(self, data):
        order = self.repo.save(data)
        self.mailer.send_confirmation(order)
        return order

問題:OrderServicePostgresOrderRepositoryGmailMailer 緊密耦合——

  • 測試時沒有辦法換成 fake repository
  • 切換到 MySQL 要改 OrderService 的程式碼(違反 OCP)
  • 初始化的複雜度散布在各個 class 裡

三種 DI 的方式

Constructor Injection(推薦):依賴透過建構子傳入,依賴是必要的且不可變。

class OrderService:
    def __init__(self, repo: OrderRepository, mailer: Mailer):
        self.repo = repo
        self.mailer = mailer
 
# 外部負責組裝
service = OrderService(
    repo=PostgresOrderRepository(),
    mailer=GmailMailer()
)

Setter Injection:透過 setter 方法設定,允許可選依賴,但狀態可能不一致。

Field Injection(框架特有):直接注入到 field(Spring 的 @Autowired),便利但難測試。


DI Container

手動組裝依賴樹很快就變得複雜。DI Container(IoC Container)自動解析依賴關係:

Spring(Java)

@Service
public class OrderService {
    @Autowired  // Spring 自動注入
    private OrderRepository repo;
    
    @Autowired
    private Mailer mailer;
}
 
@Configuration
class AppConfig {
    @Bean
    public OrderRepository orderRepository() {
        return new PostgresOrderRepository(dataSource());
    }
}

NestJS(TypeScript)

@Injectable()
class OrderService {
    constructor(
        private repo: OrderRepository,
        private mailer: Mailer,
    ) {}
}
 
@Module({
    providers: [OrderService, PostgresOrderRepository, GmailMailer],
})
class AppModule {}

Python(手動或 dependency-injector 套件)

from dependency_injector import containers, providers
 
class Container(containers.DeclarativeContainer):
    config = providers.Configuration()
    repo = providers.Singleton(PostgresOrderRepository, url=config.db.url)
    mailer = providers.Singleton(GmailMailer, api_key=config.gmail.key)
    order_service = providers.Factory(OrderService, repo=repo, mailer=mailer)

測試的優勢

DI 的最大好處是可測試性

def test_order_creation():
    # 用 fake/mock 替換真實依賴
    fake_repo = InMemoryOrderRepository()
    fake_mailer = FakeMailer()
    
    service = OrderService(repo=fake_repo, mailer=fake_mailer)
    order = service.create_order({"product_id": 1, "quantity": 2})
    
    assert order.id is not None
    assert fake_mailer.sent_emails == 1  # 驗證行為,不依賴真實 email

這個測試不需要資料庫連線、不需要 Gmail 帳號、不會有外部副作用——執行毫秒級,可以在 CI 裡大量跑。


IoC(Inversion of Control)的關係

DI 是 IoC(控制反轉)的一種實作。傳統是「你 new 你的依賴」(你控制依賴的創建),IoC 是「Container 給你依賴」(控制反轉給 Container)。Spring Framework 的另一個名字就是 IoC Container。