Dependency Injection & IoC Explained

Giới thiệu
Năm 2004, Martin Fowler xuất bản Inversion of Control Containers and the Dependency Injection pattern — bài viết trở thành tài liệu tham khảo kinh điển về cách các framework hiện đại kết nối component với nhau. Nếu bạn đã từng dùng Spring, NestJS, Angular, hay FastAPI, thì bạn đã dùng những concept từ bài viết này.
Câu hỏi cốt lõi mà Fowler đặt ra: Làm sao assemble một application từ những component loosely coupled? Làm sao một class lấy được những collaborator mà nó cần mà không biết concrete implementation của chúng?
Hai pattern ra đời để trả lời: Dependency Injection và Service Locator. Bài viết này sẽ phân tích cả hai, so sánh chúng, và chỉ ra khi nào nên dùng cái nào.
Bạn Sẽ Học Được Gì
✅ Tại sao Inversion of Control quan trọng cho real-world application
✅ Ba hình thức Dependency Injection: Constructor, Setter, Interface
✅ Service Locator pattern hoạt động thế nào như một alternative
✅ Khi nào chọn DI thay vì Service Locator (và ngược lại)
✅ Trade-off giữa Constructor và Setter Injection
✅ Nguyên tắc quan trọng hơn cả hai pattern
Yêu Cầu Trước
- Nắm vững SOLID Principles, đặc biệt Dependency Inversion
- Thoải mái với Polymorphism và Interfaces
Bài Toán: Plugin Assembly
Tưởng tượng bạn đang xây một app gợi ý phim. Bạn có class MovieLister để tìm phim theo đạo diễn:
interface MovieFinder {
findAll(): Movie[];
}
class MovieLister {
private finder: MovieFinder;
constructor() {
// ❌ Dependency bị hardcode
this.finder = new CSVMovieFinder("movies.csv");
}
moviesDirectedBy(director: string): Movie[] {
return this.finder.findAll().filter(m => m.director === director);
}
}Code này chạy được — cho đến khi bạn cần các implementation khác nhau của MovieFinder:
| Môi trường | MovieFinder Implementation |
|---|---|
| Development | CSVMovieFinder (đọc file CSV local) |
| Production | DatabaseMovieFinder (query PostgreSQL) |
| Testing | InMemoryMovieFinder (data test cứng) |
| Partner API | RemoteMovieFinder (gọi service bên ngoài) |
MovieLister phải hoạt động với bất kỳ implementation nào. Nhưng dòng new CSVMovieFinder() trong constructor khiến việc swap trở nên bất khả thi.
Đây là plugin problem: làm sao assemble các component để MovieLister hoạt động với bất kỳ MovieFinder nào, quyết định lúc deploy chứ không phải lúc compile?
Dấu hỏi chính là vấn đề: ai quyết định dùng implementation nào, và làm sao nó được wire vào?
Inversion of Control: Ý Tưởng Cốt Lõi
Thuật ngữ "Inversion of Control" (IoC) mô tả sự thay đổi về ai kiểm soát flow:
| Traditional Control | Inverted Control |
|---|---|
| Code của bạn gọi library function | Framework gọi code của bạn |
| Class của bạn tự tạo dependency | Thứ khác cung cấp dependency |
| Bạn kiểm soát việc assembly | Một external assembler kiểm soát wiring |
IoC là một principle rộng — event-driven programming, template method, plugin architecture đều dùng nó. Nhưng riêng cho component assembly, Fowler đặt ra thuật ngữ chính xác hơn: Dependency Injection.
Dependency Injection là pattern trong đó một external assembler cung cấp (inject) dependency cho component, thay vì component tự tạo.
Insight cốt lõi: tách biệt configuration của dependency khỏi việc sử dụng nó.
Ba Hình Thức Dependency Injection
Fowler xác định ba cách inject dependency. Cả ba đều giải quyết cùng một problem — chúng khác nhau ở cơ chế.
Hình Thức 1: Constructor Injection
Dependency được truyền qua constructor. Object không thể tạo ra nếu thiếu dependency.
// Interface
interface MovieFinder {
findAll(): Movie[];
}
// Component — nhận dependency qua constructor
class MovieLister {
private finder: MovieFinder;
constructor(finder: MovieFinder) {
this.finder = finder;
}
moviesDirectedBy(director: string): Movie[] {
return this.finder.findAll().filter(m => m.director === director);
}
}
// Các implementation khác nhau
class CSVMovieFinder implements MovieFinder {
private filename: string;
constructor(filename: string) {
this.filename = filename;
}
findAll(): Movie[] {
// Đọc phim từ file CSV
return parseCSV(this.filename);
}
}
class DatabaseMovieFinder implements MovieFinder {
constructor(private connectionString: string) {}
findAll(): Movie[] {
// Query database
return queryDB(this.connectionString);
}
}Assembler (container) wire mọi thứ lại:
// Assembler — biết về concrete type
class Container {
private lister: MovieLister;
private finder: MovieFinder;
constructor() {
// Configuration xảy ra Ở ĐÂY, không phải bên trong MovieLister
this.finder = new CSVMovieFinder("movies.csv");
this.lister = new MovieLister(this.finder);
}
getMovieLister(): MovieLister {
return this.lister;
}
}
// Sử dụng
const container = new Container();
const lister = container.getMovieLister();
const kubrickMovies = lister.moviesDirectedBy("Kubrick");Ưu điểm:
- Object được khởi tạo đầy đủ ngay lúc tạo — không có trạng thái nửa vời
- Dependency hiện rõ ràng trong constructor signature
- Field có thể
readonly/ immutable - Không thể quên set dependency
Đây là hình thức phổ biến nhất. Các framework như NestJS, Angular, Spring đều dùng constructor injection làm cơ chế chính.
Hình Thức 2: Setter Injection
Dependency được cung cấp qua setter method sau khi tạo object:
class MovieLister {
private finder!: MovieFinder;
// Setter để injection
setFinder(finder: MovieFinder): void {
this.finder = finder;
}
moviesDirectedBy(director: string): Movie[] {
return this.finder.findAll().filter(m => m.director === director);
}
}
// Assembler dùng setter
class Container {
private lister: MovieLister;
constructor() {
this.lister = new MovieLister();
const finder = new CSVMovieFinder("movies.csv");
this.lister.setFinder(finder);
}
getMovieLister(): MovieLister {
return this.lister;
}
}Trong các framework như Spring, trước đây cấu hình qua XML:
<bean id="movieFinder" class="CSVMovieFinder">
<property name="filename" value="movies.csv" />
</bean>
<bean id="movieLister" class="MovieLister">
<property name="finder" ref="movieFinder" />
</bean>Ưu điểm:
- Named setter cho thấy rõ mỗi dependency là gì
- Hoạt động tốt hơn khi class có nhiều optional dependency
- Đơn giản hơn với deep inheritance hierarchy (không cần chain super constructor)
Nhược điểm:
- Object có thể tồn tại trong trạng thái chưa hoàn chỉnh (finder chưa được set)
- Khó enforce required dependency hơn
Hình Thức 3: Interface Injection
Component implement một injection interface cụ thể, làm yêu cầu dependency trở nên rõ ràng:
// Injection interface — "Tôi cần MovieFinder"
interface InjectMovieFinder {
injectMovieFinder(finder: MovieFinder): void;
}
// Component implement injection interface
class MovieLister implements InjectMovieFinder {
private finder!: MovieFinder;
injectMovieFinder(finder: MovieFinder): void {
this.finder = finder;
}
moviesDirectedBy(director: string): Movie[] {
return this.finder.findAll().filter(m => m.director === director);
}
}
// Assembler kiểm tra component implement injection interface nào
// rồi gọi method injection tương ứngĐây là hình thức ít phổ biến nhất. Nó đòi hỏi nhiều ceremony hơn (thêm interface) mà lợi ích không hơn constructor injection. Bạn hiếm khi thấy nó trong framework hiện đại.
So Sánh Ba Hình Thức
| Khía cạnh | Constructor | Setter | Interface |
|---|---|---|---|
| Object completeness | Luôn khởi tạo đầy đủ | Có thể chưa hoàn chỉnh | Có thể chưa hoàn chỉnh |
| Immutability | Hỗ trợ readonly field | Mutable tự nhiên | Mutable tự nhiên |
| Nhiều dependency | Constructor signature dài | Named setter gọn gàng | Verbose (một interface/dep) |
| Framework áp dụng | NestJS, Angular, Spring (hiện đại) | Spring (legacy XML) | Hiếm |
| Fowler khuyên | Bắt đầu ở đây | Chuyển khi constructor quá dài | Tránh |
Alternative: Service Locator
Dependency Injection không phải giải pháp duy nhất. Pattern Service Locator tiếp cận khác: thay vì dependency được push đến bạn, bạn tự pull chúng từ một central registry.
Service Locator Cơ Bản
// Service locator — central registry
class ServiceLocator {
private static services = new Map<string, unknown>();
static register(key: string, service: unknown): void {
this.services.set(key, service);
}
static get<T>(key: string): T {
const service = this.services.get(key);
if (!service) throw new Error(`Service not found: ${key}`);
return service as T;
}
}
// Configuration — đăng ký implementation
ServiceLocator.register("movieFinder", new CSVMovieFinder("movies.csv"));
// Component tự pull dependency
class MovieLister {
private finder: MovieFinder;
constructor() {
this.finder = ServiceLocator.get<MovieFinder>("movieFinder");
}
moviesDirectedBy(director: string): Movie[] {
return this.finder.findAll().filter(m => m.director === director);
}
}Service Locator Type-Safe
Cách tốt hơn dùng typed method thay vì string key:
class ServiceLocator {
private static movieFinder: MovieFinder;
static registerMovieFinder(finder: MovieFinder): void {
this.movieFinder = finder;
}
static getMovieFinder(): MovieFinder {
return this.movieFinder;
}
}
// Sử dụng
ServiceLocator.registerMovieFinder(new CSVMovieFinder("movies.csv"));
class MovieLister {
private finder: MovieFinder;
constructor() {
this.finder = ServiceLocator.getMovieFinder();
}
}Segregated Interface Locator
Với application lớn, chia locator thành interface tập trung:
interface MovieServices {
getMovieFinder(): MovieFinder;
}
interface UserServices {
getUserRepository(): UserRepository;
}
// MovieLister chỉ thấy MovieServices, không thấy toàn bộ locator
class MovieLister {
private finder: MovieFinder;
constructor(locator: MovieServices) {
this.finder = locator.getMovieFinder();
}
}Điều này giới hạn những gì mỗi component có thể access — component chỉ cần MovieFinder thì không thể chạm vào UserRepository.
Service Locator vs Dependency Injection
Đây là phần so sánh trọng tâm trong bài viết của Fowler. Cả hai pattern đều đạt được cùng mục tiêu — decouple component khỏi concrete implementation của dependency. Khác biệt nằm ở cơ chế:
Dependency Injection — Container push dependency vào component:
Service Locator — Component tự pull dependency từ registry:
| Khía cạnh | Service Locator | Dependency Injection |
|---|---|---|
| Hướng | Component pull (hỏi xin dependency) | Container push (cung cấp dependency) |
| Phụ thuộc vào pattern | Component phụ thuộc Locator | Component không có framework dependency |
| Visibility | Rõ ràng — thấy lookup trong code | Ngầm — dependency "tự xuất hiện" |
| Testability | Swap registration trong test | Truyền mock qua constructor |
| Reusability | Component gắn với locator API | Component hoạt động ở đâu cũng được |
| Debug | Follow call get() | Check container configuration |
Khi Nào Dùng Cái Nào?
Insight cốt lõi của Fowler: lựa chọn phụ thuộc vào ai sẽ sử dụng component.
Dùng Service Locator khi:
- Bạn kiểm soát toàn bộ code sử dụng component
- Component dùng trong một application duy nhất
- Bạn muốn dependency resolution rõ ràng, explicit
- Bạn thích mental model đơn giản hơn (chỉ cần look up)
Dùng Dependency Injection khi:
- Component sẽ được người khác dùng (library, framework, package)
- Bạn không kiểm soát application sử dụng component của bạn
- Bạn muốn zero coupling với bất kỳ container hay locator nào
- Component cần hoạt động trong nhiều deployment context khác nhau
"Khác biệt cốt lõi là với Service Locator, mọi consumer đều phụ thuộc vào locator. Locator có thể ẩn dependency đến implementation khác, nhưng bạn vẫn cần thấy locator. Vì vậy quyết định giữa locator và injector phụ thuộc vào việc dependency đó có phải problem hay không." — Martin Fowler
Constructor vs Setter Injection: Quyết Định Thực Tế
Ngay cả sau khi chọn Dependency Injection, bạn còn phải quyết định thêm: inject bằng cách nào?
Ưu Tiên Constructor Injection Khi:
// ✅ Contract rõ ràng — bạn CẦN những thứ này để hoạt động
class OrderProcessor {
constructor(
private readonly paymentGateway: PaymentGateway,
private readonly inventory: InventoryService,
private readonly notifier: NotificationService
) {}
processOrder(order: Order): void {
this.inventory.reserve(order.items);
this.paymentGateway.charge(order.total);
this.notifier.sendConfirmation(order);
}
}- Dependency là required — object vô nghĩa nếu thiếu chúng
- Bạn muốn immutability (
readonlyfield) - Bạn muốn enforce valid state lúc tạo object
- Constructor có số lượng parameter hợp lý (3-5)
Cân Nhắc Setter Injection Khi:
// ✅ Nhiều optional configuration point
class ReportGenerator {
private formatter: Formatter = new DefaultFormatter();
private exporter: Exporter = new PDFExporter();
private locale: string = "en";
setFormatter(formatter: Formatter): void { this.formatter = formatter; }
setExporter(exporter: Exporter): void { this.exporter = exporter; }
setLocale(locale: string): void { this.locale = locale; }
generate(data: ReportData): Report {
const formatted = this.formatter.format(data, this.locale);
return this.exporter.export(formatted);
}
}- Dependency là optional với default hợp lý
- Có nhiều configuration point (10+ constructor param là code smell)
- Deep inheritance hierarchy khiến constructor chaining trở nên đau đầu
Khuyến Nghị Của Fowler
Bắt đầu với constructor injection. Chuyển sang setter injection khi constructor parameter trở nên quá nhiều.
Phần lớn framework hiện đại đồng ý — constructor injection là default, setter injection là escape hatch.
Configuration: Code vs File
Một cân nhắc cuối cùng từ bài viết: bạn cấu hình wiring bằng cách nào?
Programmatic Configuration (Code)
// config.ts — toàn bộ wiring trong code
function createContainer(): Container {
const finder = new DatabaseMovieFinder(process.env.DB_URL!);
const lister = new MovieLister(finder);
return { lister };
}Ưu điểm: Type-safe, refactor được, IDE support, conditional logic dễ dàng.
Declarative Configuration (File)
{
"services": {
"movieFinder": {
"class": "DatabaseMovieFinder",
"args": ["${DB_URL}"]
},
"movieLister": {
"class": "MovieLister",
"inject": ["movieFinder"]
}
}
}Ưu điểm: Thay đổi behavior không cần recompile, non-developer có thể chỉnh.
Lời Khuyên Của Fowler
Cung cấp programmatic API trước. Thêm configuration file như optional convenience. Nếu configuration phức tạp đến mức cần conditional và loop, thì bạn đã outgrow configuration file — hãy dùng một programming language đúng nghĩa.
Phần lớn framework hiện đại theo lời khuyên này: NestJS dùng decorator (code), Spring Boot chuyển từ XML sang annotation (code), FastAPI dùng Python function (code).
DI Ngoài Thực Tế: Các Framework Triển Khai Thế Nào
Bài viết của Fowler không chỉ mô tả pattern — nó trở thành blueprint mà cả ngành công nghiệp áp dụng. PicoContainer và Spring là hai container mà Fowler phân tích trong bài viết gốc năm 2004. Một cuộc họp về DI tại ThoughtWorks vào tháng 12 năm 2003 giữa developer PicoContainer, Rod Johnson (Spring), và Fowler (qua mạng) chính là nơi sản sinh bài viết và đặt ra thuật ngữ "Dependency Injection."
Đây là cách các framework lớn implement DI ngày nay — và họ chọn hình thức nào từ bài viết của Fowler:
Framework Trực Tiếp Trích Dẫn Bài Viết Của Fowler
| Framework | Ngôn ngữ | Hình thức DI | Ghi chú |
|---|---|---|---|
| Spring | Java | Constructor + Setter | Spring docs trích dẫn trực tiếp bài viết của Fowler. Một trong hai container gốc được phân tích |
| PicoContainer | Java | Constructor (chính) | Container còn lại trong bài viết. Tiên phong constructor injection |
| Yii2 | PHP | Constructor + Setter + Property | Link trực tiếp bài viết Fowler. Cũng implement Service Locator trên nền DI container |
| ASP.NET Core | C# | Constructor (chính) | Microsoft docs trích dẫn "Inversion of Control Containers and the Dependency Injection Pattern" |
| Symfony | PHP | Constructor + Setter | Creator Fabien Potencier viết series về DI tham khảo Fowler |
| Spring.NET | C# | Constructor + Setter | Bản .NET của Spring, tham khảo trực tiếp Fowler |
Framework Xây Dựng Trên Cùng Ý Tưởng
| Framework | Ngôn ngữ | Hình thức DI | Cách hoạt động |
|---|---|---|---|
| Angular | TypeScript | Constructor | Dùng decorator (@Injectable), hierarchical injector tree |
| NestJS | TypeScript | Constructor | Mô phỏng theo DI system của Angular, dùng @Injectable decorator |
| Laravel | PHP | Constructor | Service Container với auto-resolution. Cũng dùng Service Locator qua app() helper |
| FastAPI | Python | Function-based | Cách tiếp cận Python — Depends() inject qua function parameter |
| Google Guice | Java | Constructor | Alternative nhẹ hơn Spring, dùng annotation |
Xu Hướng Chung
Gần như mọi framework đều theo khuyến nghị của Fowler: constructor injection làm cơ chế chính, với setter injection làm fallback. Bài viết đã chuẩn hóa bộ từ vựng (constructor injection, setter injection, service locator) mà cả ngành giờ đều dùng.
Điều thú vị là Yii2 là một trong số ít framework explicitly implement cả DI Container lẫn Service Locator song song — với Service Locator xây trên nền DI container. Phần lớn framework khác chọn một cái (thường là DI) rồi chạy luôn, dù Laravel làm mờ ranh giới với app() facade hoạt động như một service locator.
Nguyên Tắc Quan Trọng Nhất
Sau tất cả những so sánh, Fowler đúc kết takeaway quan trọng nhất — và nó không phải về việc chọn DI hay Service Locator:
"Điều quan trọng là tách biệt service configuration khỏi việc sử dụng. Các pattern này là hệ quả tự nhiên của sự tách biệt đó."
Dù bạn dùng constructor injection, setter injection, hay service locator, nguyên tắc cơ bản vẫn như nhau: code sử dụng dependency không nên là code quyết định dùng concrete implementation nào.
// ❌ Use và configuration đan xen
class MovieLister {
private finder = new CSVMovieFinder("movies.csv"); // Configuration
moviesDirectedBy(director: string): Movie[] { // Use
return this.finder.findAll().filter(m => m.director === director);
}
}
// ✅ Configuration tách biệt khỏi use (bằng bất kỳ pattern nào)
class MovieLister {
constructor(private finder: MovieFinder) {} // Chỉ use
moviesDirectedBy(director: string): Movie[] {
return this.finder.findAll().filter(m => m.director === director);
}
}
// Configuration xảy ra ở nơi khác — trong container, locator, hoặc factorySự tách biệt này cho phép:
- Testing: Swap implementation trong test setup
- Deployment flexibility: Implementation khác nhau cho mỗi environment
- Team autonomy: Các team sở hữu implementation riêng đằng sau interface
- Evolution: Thay implementation mà không chạm vào consumer
Tham Khảo Nhanh
Decision Flowchart
Tổng Kết Pattern
| Pattern | Cơ chế | Phù Hợp Cho |
|---|---|---|
| Constructor Injection | Dependency truyền lúc tạo object | Required dependency, immutable object |
| Setter Injection | Dependency set sau khi tạo object | Optional dependency, nhiều config point |
| Interface Injection | Component implement injection interface | Hiếm khi dùng trong code hiện đại |
| Service Locator | Component tự look up dependency | Component nội bộ trong application |
Điểm Chính
-
Bài toán cốt lõi là assemble component với các implementation khác nhau qua các deployment — "plugin problem"
-
Dependency Injection có container push dependency vào component. Service Locator có component tự pull dependency từ registry. Cả hai đều decouple configuration khỏi use
-
Constructor injection là lựa chọn mặc định — nó tạo object được khởi tạo đầy đủ, immutable với dependency contract rõ ràng
-
Setter injection là fallback khi constructor trở nên quá dài (nhiều optional dependency, deep inheritance)
-
Service Locator đơn giản hơn nhưng tạo dependency vào chính locator. Dùng nó trong application của bạn; tránh trong reusable library
-
Nguyên tắc vượt trên cả hai pattern: Tách biệt service configuration khỏi nơi sử dụng. Điều này quan trọng hơn việc bạn chọn pattern cụ thể nào
Đọc Thêm
- Bài viết gốc của Martin Fowler — Source material cho bài viết này
- SOLID Principles Explained — Dependency Inversion Principle là "tại sao" đằng sau DI
- Dependency Injection trong FastAPI — DI áp dụng trong Python framework thực tế
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.