沒有 DI 的世界
一個 UserController 需要 UserService,UserService 需要 UserRepository,UserRepository 需要資料庫連線:
// 你要手動建立每一層
const db = new Database(config.db);
const userRepo = new UserRepository(db);
const userService = new UserService(userRepo);
const userController = new UserController(userService);這個手動建立依賴的過程有幾個問題:
一、初始化順序你要管。db 要在 userRepo 之前建立,userRepo 要在 userService 之前建立。順序錯了就 crash。
二、測試要替換依賴很麻煩。你想 unit test UserService,要 mock UserRepository,你必須把 mock 傳進去——但如果 UserService 直接在內部 new UserRepository(),你根本傳不進去。
// 這樣沒辦法 mock repository
class UserService {
private repo = new UserRepository(); // ← 直接 new,test 無法替換
async findUser(id: number) {
return this.repo.findById(id);
}
}三、大型系統的依賴圖變得很複雜。一個 Controller 可能依賴 3 個 Service,每個 Service 又依賴 2-3 個 Repository,Repository 依賴 DB 和 Cache——手動管理這個圖,漏一個就 null reference。
DI 是為了解這三個問題。
DI Container 的運作原理
DI Container(或叫 IoC Container)是一個「知道如何建立所有物件的工廠」:
- 你告訴 container「這些 class 是可以被注入的」
- Container 自己分析依賴圖,決定初始化順序
- 當某個 class 被需要時,container 自動建立它(如果還沒建立)並傳進去
// NestJS — 你只宣告需要什麼,不用管怎麼建
@Injectable()
class UserRepository {
constructor(
@InjectRepository(User) private repo: Repository<User>
) {}
}
@Injectable()
class UserService {
constructor(private userRepo: UserRepository) {} // ← container 自動注入
}
@Controller('users')
class UserController {
constructor(private userService: UserService) {} // ← container 自動注入
}Container 看到 UserController 需要 UserService,UserService 需要 UserRepository,自動按正確順序建立,不用你管初始化順序。
Spring 的 DI
Spring 的 DI 是 Java 後端的標準做法,有更完整的生命週期管理:
@Repository
public class UserRepository {
@Autowired
private JpaRepository<User, Long> repo; // ← Spring 注入
}
@Service
public class UserService {
@Autowired
private UserRepository userRepo; // ← Spring 注入
}
@RestController
public class UserController {
@Autowired
private UserService userService; // ← Spring 注入
}Spring 還有更細緻的 bean scope:
@Singleton(預設):整個 application 只有一個實例@Prototype:每次注入都建新的實例@RequestScoped:每個 HTTP request 一個實例(Web 場景常用)
FastAPI 的 DI:Depends()
FastAPI 沒有 class-based DI container,但有 Depends() 這個機制:
# 定義一個「依賴」
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
def get_current_user(token: str = Header(...), db = Depends(get_db)):
user = db.query(User).filter(User.token == token).first()
if not user:
raise HTTPException(status_code=401)
return user
# 在 route handler 使用
@app.get("/users/me")
def get_me(current_user = Depends(get_current_user)):
return current_userFastAPI 的 Depends() 是 function-based 的,不像 NestJS / Spring 是 class-based。它解決了「每個 request 需要建立 DB session、驗 auth token」的問題,但不是完整的 IoC container。
沒有 DI Container 的框架怎麼辦(Express / Gin)
Express 和 Gin 都沒有 DI container,但這不代表不能管好依賴。常見的做法有兩種:
方法一:手動 singleton + module export
// src/repositories/user.repository.ts
import { db } from '../database'; // ← singleton db 連線
export class UserRepository {
async findById(id: number) {
return db.query('SELECT * FROM users WHERE id = ?', [id]);
}
}
export const userRepository = new UserRepository(); // ← 模組層級 singleton
// src/services/user.service.ts
import { userRepository } from '../repositories/user.repository';
export class UserService {
async getUser(id: number) {
return userRepository.findById(id);
}
}
export const userService = new UserService();這個做法讓每個 module 負責自己的 singleton,import 就能用,不需要 container。
方法二:在 bootstrap 層手動建立依賴圖
Proto 的做法是在 bootstrap 時手動建立:
// src/bootstrap/index.ts
const db = await initializeDatabase();
const userRepo = new UserRepository(db);
const userService = new UserService(userRepo);
const userController = new UserController(userService);
// 再把 controller 傳給 router...這比方法一更 explicit,但初始化程式碼會比較長。好處是依賴圖在 bootstrap 一個地方就看清楚了。
DI 對測試的影響
DI 最大的好處是讓 unit test 更容易:
// NestJS:用 testing module 替換依賴
const module = await Test.createTestingModule({
providers: [
UserService,
{
provide: UserRepository,
useValue: { findById: jest.fn().mockResolvedValue(mockUser) }, // ← mock
},
],
}).compile();
const service = module.get<UserService>(UserService);// Express 手動 DI:直接傳 mock
const mockRepo = { findById: jest.fn().mockResolvedValue(mockUser) };
const service = new UserService(mockRepo); // ← constructor injection
const result = await service.getUser(1);
expect(mockRepo.findById).toHaveBeenCalledWith(1);Constructor injection(把依賴當 constructor 參數傳進去)在有沒有 DI container 的框架裡都適用,也是最容易 mock 的設計。如果你在 Express 專案裡用 new Dependency() 直接在 class 內部建立依賴,測試會很痛。
Express 的依賴管理
[[backend/framework/express/base-repository-service|[express][M4] Base Repository + Base Service]] 有 proto 的具體實作——如何在沒有 DI container 的 Express 專案裡,用 constructor injection 讓每一層都可以被 mock 測試。
