Inversion of Control — đảo ngược quyền kiểm soát
IoC là design principle: quyền tạo và nối dependency bị đảo từ code bạn viết sang container. Bài này giải thích bản chất IoC, lý do cần IoC (testability, decoupling), và cơ chế trước/sau khi áp dụng.
TL;DR: IoC (Inversion of Control) là design principle — quyền điều khiển việc tạo và nối dependency bị đảo ngược từ code bạn viết sang một bên ngoài (container/framework). Trước IoC, class tự new dependency; sau IoC, container quyết định tạo cái gì, khi nào, nối với ai. Lợi ích cốt lõi: class không còn biết implementation cụ thể của dependency → testable và decoupled. Dependency Injection là hình thức IoC phổ biến nhất Spring dùng để truyền dependency vào object.
Bài trước (Spring là gì) cho thấy Spring là IoC container. Bài này trả lời đúng một câu hỏi: IoC là gì và vì sao cần nó? Cơ chế container bên dưới (BeanFactory, singletonObjects map) được mổ tách ở BeanFactory vs ApplicationContext.
1. Analogy — đặt cơm vs tự nấu cơm
Bạn có hai cách ăn trưa:
Cách A — tự nấu: đi chợ, chọn rau thịt, sơ chế, nấu, dọn bàn. Mọi bước bạn quyết định và thực hiện — bạn kiểm soát toàn bộ flow.
Cách B — đặt GrabFood: bạn chỉ nói "tôi muốn cơm tấm sườn". Grab tìm quán, điều shipper, giao đúng giờ. Bạn không quyết định khi nào nấu, ai nấu, đi đường nào — quyền điều phối đó bị "đảo" sang Grab.
Trong software, tình huống tương tự:
| Đời thường | Software |
|---|---|
| Tự nấu cơm | Class tự new PaymentGateway(), tự gọi từng bước |
| Đặt GrabFood | Class khai báo "tôi cần PaymentGateway", container tạo và đưa vào |
| Bạn quyết toàn bộ flow | Traditional: caller drives, code bạn khởi tạo mọi thứ |
| Grab quyết flow | Inverted: container drives, framework tạo và nối object cho bạn |
flowchart LR
subgraph A["Truoc IoC -- ban tu quyet"]
direction TB
Code["OrderService.java"]
Code -->|"new StripePayment()"| SP["StripePayment"]
Code -->|"new EmailService()"| ES["EmailService"]
Code -->|"new Inventory()"| INV["Inventory"]
end
subgraph B["Sau IoC -- container quyet"]
direction TB
CTR["Spring Container"]
OS2["OrderService"]
CTR -->|"tao + inject"| OS2
CTR -->|"tao + noi"| SP2["StripePayment"]
CTR -->|"tao + noi"| ES2["EmailService"]
CTR -->|"tao + noi"| INV2["Inventory"]
endIoC = ai-quyết-flow bị đảo ngược. Trước: code bạn drive mọi thứ. Sau: container drive, code bạn chỉ khai báo nhu cầu.
2. Định nghĩa IoC chính xác
IoC (Inversion of Control) là design principle phát biểu: quyền kiểm soát việc tạo object và nối dependency được đảo ngược từ code bạn viết sang một thành phần bên ngoài (container, framework).
Thuật ngữ này không phải của Spring. Martin Fowler hệ thống hoá IoC năm 2004 trong bài viết kinh điển "Inversion of Control Containers and the Dependency Injection pattern". Fowler chỉ ra rằng "IoC" quá rộng — nó bao gồm nhiều kỹ thuật:
| Kỹ thuật | Cách đảo flow | Ví dụ |
|---|---|---|
| Dependency Injection | Container tạo dependency và đưa vào constructor/setter | @Autowired, Spring container |
| Service Locator | Object hỏi locator "cho tôi service X" | ApplicationContext.getBean() |
| Template Method | Framework định nghĩa skeleton, gọi vào hook bạn override | JdbcTemplate, RestTemplate |
| Event / Callback | Bạn đăng ký listener, framework gọi khi event xảy ra | @EventListener |
DI chỉ là một trong nhiều hình thức IoC. Spring dùng cả bốn — nhưng Dependency Injection là hình thức chủ yếu để quản lý bean.
Trong docs Spring, "IoC container" tức là container làm tất cả bốn việc trên. Khi bạn nói "Spring là DI container" — đúng nhưng hẹp hơn.
3. Tại sao cần IoC — vấn đề khi tự quản lý dependency
Giả sử bạn có OrderService cần PaymentGateway và InventoryClient:
// Truoc IoC — OrderService tu quyet moi thu
public class OrderService {
private final PaymentGateway payment;
private final InventoryClient inventory;
public OrderService() {
// Kho khan 1: bi rang buoc vao implementation cu the
this.payment = new StripePayment(System.getenv("STRIPE_KEY"));
this.inventory = new HttpInventoryClient("http://inventory-svc");
}
public void placeOrder(Order order) {
inventory.reserve(order);
payment.charge(order.total());
}
}
Ba vấn đề cụ thể:
Vấn đề 1 — Testability bằng 0: Để unit test placeOrder, bạn cần chạy thật StripePayment (gọi API Stripe thật) và HttpInventoryClient (cần server inventory thật). Không có cách nào thay bằng mock mà không sửa source code.
Vấn đề 2 — Coupling chặt: OrderService phụ thuộc trực tiếp vào StripePayment — muốn đổi sang MoMoPayment phải sửa bên trong OrderService. Mỗi lần đổi implementation là mỗi lần sửa code business.
Vấn đề 3 — Lifecycle không kiểm soát được: Mỗi lần tạo OrderService là tạo mới StripePayment và HttpInventoryClient. Không có cơ chế reuse, không có cách biết bao nhiêu instance đang tồn tại, không clean up đúng lúc.
4. Sau IoC — quyền tạo dependency chuyển sang container
Cùng nghiệp vụ, viết lại theo IoC:
// Sau IoC — OrderService chi khai bao nhu cau
@Service
public class OrderService {
private final PaymentGateway payment;
private final InventoryClient inventory;
// Khai bao "toi can 2 dependency nay"
// Container quyet tao gi, khi nao, inject vao day
public OrderService(PaymentGateway payment, InventoryClient inventory) {
this.payment = payment;
this.inventory = inventory;
}
public void placeOrder(Order order) {
inventory.reserve(order);
payment.charge(order.total());
}
}
OrderService không biết StripePayment tồn tại. Nó chỉ biết "tôi cần một PaymentGateway". Container quyết implementation nào được đưa vào.
Vấn đề 1 giải quyết — test dễ: Vì dependency đến từ ngoài, unit test có thể tạo mock:
// Test khong can Spring, khong can server that
@Test
void placeOrder_should_reserve_then_charge() {
var mockPayment = mock(PaymentGateway.class);
var mockInventory = mock(InventoryClient.class);
var service = new OrderService(mockPayment, mockInventory);
service.placeOrder(sampleOrder());
verify(mockInventory).reserve(sampleOrder());
verify(mockPayment).charge(sampleOrder().total());
}
Vấn đề 2 giải quyết — swap implementation không đụng OrderService: Đổi từ Stripe sang MoMo chỉ cần đăng ký MoMoPayment implements PaymentGateway — container inject đúng bean, OrderService không hay biết.
Vấn đề 3 giải quyết — container quản lý lifecycle: Spring quyết singleton hay prototype, khởi tạo khi nào, destroy khi nào. Code business không liên quan.
5. Cơ chế — ai giữ quyền control trước và sau
Đây là phần bóc rõ "inversion" thật sự diễn ra ở đâu:
sequenceDiagram participant Dev as Code ban viet participant Cnt as Spring Container participant Obj as Bean (OrderService, etc.) Note over Dev,Cnt: Truoc IoC -- Dev quyet het Dev->>Obj: new StripePayment(key) Dev->>Obj: new OrderService(payment, inventory) Dev->>Obj: orderService.placeOrder(order) Note over Dev,Cnt: Sau IoC -- Container quyet Dev->>Cnt: @Service, @Component (khai bao) Cnt->>Cnt: doc khai bao, xac dinh dependency graph Cnt->>Obj: tao StripePayment (inject key tu config) Cnt->>Obj: tao OrderService (inject payment + inventory) Dev->>Obj: orderService.placeOrder(order)
Trước IoC: code bạn viết (hàm main, App.java) nắm quyền: gọi new, quyết thứ tự, quản lý instance, truyền config vào. Caller drives.
Sau IoC: quyền đó chuyển hoàn toàn sang container. Bạn chỉ làm 2 việc:
- Khai báo class là bean (
@Service,@Component). - Khai báo nhu cầu (constructor parameter).
Container đọc khai báo, xây dựng dependency graph, quyết thứ tự tạo, inject, quản lý lifecycle. Code bạn không gọi new một lần nào cho bean.
"Inversion" chính là: trước đây bạn gọi vào library/class khác; bây giờ container gọi vào code của bạn — bạn cung cấp logic, framework orchestrate.
Service Locator cũng là IoC — code hỏi container "cho tôi bean X" (ctx.getBean(PaymentGateway.class)). Nhưng dependency vẫn ẩn bên trong method, không hiện ở constructor → khó test, khó trace. Đây là lý do Dependency Injection được ưa hơn Service Locator — dependency rõ ràng tại constructor.
6. Pitfall — nhầm IoC với DI
❌ Nhầm thường gặp: Cho rằng IoC và DI là một.
// Day la IoC nhung KHONG phai DI:
@Component
public class OrderProcessor {
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
// Container goi vao method nay khi event xay ra
// Flow bi dao nguoc (IoC), nhung khong co dependency nao duoc inject
}
}
Spring gọi vào onOrderCreated khi event xảy ra — đây là IoC kiểu callback. Không có dependency nào được inject → không phải DI.
✅ Hiểu đúng: IoC là principle. DI là một cách implement IoC (cách phổ biến nhất trong Spring). Container Spring thực hiện IoC theo nhiều hình thức cùng lúc.
❌ Nhầm thứ hai: Inject ApplicationContext rồi gọi getBean() trong code business.
@Service
public class BadService {
@Autowired
private ApplicationContext ctx; // anti-pattern
public void doWork() {
var p = ctx.getBean(PaymentGateway.class); // service locator
p.charge(amount);
}
}
✅ Inject PaymentGateway thẳng vào constructor. ctx.getBean() ẩn dependency, làm hỏng testability — phủ nhận lợi ích chính của IoC. Xem chi tiết 3 hình thức inject ở bài Dependency Injection.
Liên hệ các bài khác
Bài này là một mảnh trong chuỗi container. Ghép với các bài xung quanh:
- Spring là gì: bài trước đặt context "Spring là IoC container" — bài này giải thích IoC nghĩa là gì để câu đó không còn mơ hồ.
- Dependency Injection: bài tiếp theo mổ cụ thể hình thức IoC Spring dùng chính — 3 cách inject (constructor/setter/field), thuật toán resolve dependency, và vì sao constructor được khuyến nghị.
- BeanFactory vs ApplicationContext: sau khi hiểu IoC về mặt principle, bài đó bóc cơ chế bên dưới container —
singletonObjectsmap,beanDefinitionMap, và tại saogetBean()lần hai nhanh hơn lần đầu.
Tóm tắt
- IoC là design principle: quyền tạo và nối dependency đảo từ code bạn viết sang container/framework.
- Trước IoC: class tự
newdependency → coupling chặt, không testable, lifecycle không kiểm soát. - Sau IoC: class khai báo nhu cầu, container quyết implementation, inject, lifecycle — code business không gọi
new. - DI là một hình thức IoC, cùng với Service Locator, Template Method, Event/Callback.
- Lợi ích cốt lõi: testability (mock dễ qua constructor), decoupling (swap implementation không đụng business code), lifecycle tập trung.
- "Inversion" thật sự: trước đây bạn gọi vào library; sau IoC container gọi vào code bạn.
Tự kiểm tra
Q1Phát biểu nào mô tả đúng bản chất của IoC? (a) Framework tạo object thay bạn. (b) Quyền kiểm soát việc tạo và nối dependency chuyển từ code bạn viết sang container. (c) Code bạn không được gọi new nữa. (d) Spring quản lý database connection.▸
new nữa. (d) Spring quản lý database connection.Đáp án đúng là (b). IoC là về quyền kiểm soát flow bị đảo ngược, không chỉ đơn giản là "framework tạo object".
(a) gần đúng nhưng chưa đủ — "tạo object thay bạn" mới là hệ quả, bản chất là ai nắm quyền điều khiển. (c) sai — code bạn vẫn có thể gọi new cho object thuần (DTO, value object), IoC chỉ áp dụng cho bean được container quản lý. (d) không liên quan.
Câu (b) khớp với định nghĩa Fowler 2004: "Inversion of Control" = flow control bị đảo — không phải bạn gọi framework, mà framework gọi code bạn và quyết định wiring.
Q2Đoạn code sau có áp dụng IoC không? Nếu có, đây là hình thức nào — DI hay không phải DI? Giải thích.@Component
public class AuditService {
@EventListener
public void onLogin(UserLoginEvent event) {
log.info("User logged in: " + event.userId());
}
}
▸
@Component
public class AuditService {
@EventListener
public void onLogin(UserLoginEvent event) {
log.info("User logged in: " + event.userId());
}
}Có áp dụng IoC, nhưng KHÔNG phải DI. Đây là IoC kiểu event/callback.
Lý do là IoC: Spring gọi vào method onLogin khi UserLoginEvent được publish — flow bị đảo, bạn không viết vòng lặp poll event, framework dispatch cho bạn.
Lý do không phải DI: không có dependency nào được inject vào AuditService thông qua constructor hay setter ở đây. UserLoginEvent là tham số method, không phải dependency được container resolve.
Spring thực hiện IoC đồng thời nhiều hình thức: DI cho bean, event callback cho xử lý sự kiện, Template Method cho JdbcTemplate. Hiểu rõ sự khác biệt giúp đọc code Spring không bị mù mờ.
Q3Trước IoC, class OrderService tự new StripePayment() trong constructor. Điều này gây ra hai vấn đề cụ thể nào? Giải thích cách IoC giải quyết từng vấn đề.▸
OrderService tự new StripePayment() trong constructor. Điều này gây ra hai vấn đề cụ thể nào? Giải thích cách IoC giải quyết từng vấn đề.Vấn đề 1 — Testability bằng 0: OrderService bị gắn cứng vào StripePayment. Test phải chạy thật Stripe API — không thể thay bằng mock. Sau IoC: dependency đến từ constructor parameter → test truyền mock vào: new OrderService(mockPayment, mockInventory). Không cần Spring, không cần server thật.
Vấn đề 2 — Coupling chặt: Muốn đổi sang MoMoPayment phải sửa bên trong OrderService — vi phạm Open-Closed principle. Sau IoC: OrderService chỉ biết interface PaymentGateway. Đăng ký bean mới implement interface đó, container inject đúng bean, OrderService không hay biết.
Cả hai vấn đề đều có chung gốc: class biết quá nhiều về implementation của dependency. IoC tách biết cái gì (PaymentGateway) khỏi cái nào (StripePayment hay MoMoPayment).
Q4Đoạn code dưới inject ApplicationContext để gọi getBean(). Đây có phải IoC không? Đây có phải DI không? Vấn đề là gì?@Service
public class ReportService {
@Autowired
private ApplicationContext ctx;
public void generate(String type) {
var formatter = ctx.getBean(type, ReportFormatter.class);
formatter.format(getData());
}
}
▸
ApplicationContext để gọi getBean(). Đây có phải IoC không? Đây có phải DI không? Vấn đề là gì?@Service
public class ReportService {
@Autowired
private ApplicationContext ctx;
public void generate(String type) {
var formatter = ctx.getBean(type, ReportFormatter.class);
formatter.format(getData());
}
}Có là IoC (Service Locator), KHÔNG phải DI cho ReportFormatter.
ApplicationContext được inject vào constructor/field — đó là DI cho ctx. Nhưng ReportFormatter được lấy qua ctx.getBean(type) bên trong method — đây là Service Locator, không phải DI.
Vấn đề:
Dependency ReportFormatter ẩn trong body method — đọc class signature không biết ReportService cần ReportFormatter. Test phải mock cả ApplicationContext thay vì chỉ mock ReportFormatter. Lỗi "bean không tồn tại" xảy ra lúc runtime (generate() được gọi), không phải lúc startup.
Ngoại lệ hợp lý: trong ví dụ này, type là tham số runtime — không thể biết trước tại startup cần inject bean nào. Đây là trường hợp Service Locator chấp nhận được, nhưng nên dùng ObjectProvider hoặc Map inject thay vì ApplicationContext trực tiếp.
Q5Fowler 2004 đề xuất tên "Dependency Injection" để thay "IoC" khi nói về wiring dependency. Lý do là gì? Tên "IoC" có vấn đề gì khi dùng để mô tả Spring?▸
Vấn đề của "IoC": thuật ngữ quá rộng và mơ hồ. Trước Fowler, "IoC" được dùng cho mọi thứ "framework gọi code bạn" — từ Servlet doGet(), JUnit @Test, đến Swing event listener. Nói "Spring là IoC container" không cho biết Spring làm gì cụ thể.
Fowler đề xuất "Dependency Injection" để chỉ đúng một hình thức: container inject dependency vào object qua constructor/setter/field. Tên này mô tả cơ chế cụ thể, không phải principle chung chung.
Spring docs giữ "IoC container" vì lý do lịch sử — Rod Johnson 2003 dùng từ "IoC". Khi đọc Spring docs, "IoC" thực tế bao gồm DI + lifecycle callback + event + lookup tổng hợp. Khi muốn nói cụ thể về wiring bean, dùng "DI" chính xác hơn.
Hệ quả thực tế: khi team dùng "IoC" và "DI" hoán đổi nhau, nghĩa bị mờ. Dùng "DI" khi muốn nói về inject dependency; dùng "IoC" khi muốn nói về principle đảo flow tổng quát.
Bài tiếp theo: Dependency Injection — 3 hình thức
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên