沒有 DI 的世界

一個 UserController 需要 UserServiceUserService 需要 UserRepositoryUserRepository 需要資料庫連線:

// 你要手動建立每一層
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)是一個「知道如何建立所有物件的工廠」:

  1. 你告訴 container「這些 class 是可以被注入的」
  2. Container 自己分析依賴圖,決定初始化順序
  3. 當某個 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 需要 UserServiceUserService 需要 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_user

FastAPI 的 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 測試。