問題背景
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問題:OrderService 和 PostgresOrderRepository、GmailMailer 緊密耦合——
- 測試時沒有辦法換成 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。