Nguyễn Đình Cường
0 comment
09 Nov, 2026
Bạn viết một class gửi email chào mừng, bên trong tự tạo kết nối SMTP. Khi viết test, mỗi lần chạy lại gửi email thật, hoặc test thất bại vì không có mạng. Dependency injection (DI) là cách đơn giản để thoát khỏi tình trạng đó, và nó dễ hơn nhiều so với tên gọi nghe có vẻ học thuật.
Khi class A cần dùng class B để làm việc thì B là dependency (phụ thuộc) của A. Câu hỏi là: A có tự tạo ra B hay được cung cấp B từ bên ngoài?
class SmtpMailer {send(to, subject, body) { /* gửi email thật qua SMTP */ }}class WelcomeService {constructor() {this.mailer = new SmtpMailer(); // gắn cứng}welcome(user) {this.mailer.send(user.email, 'Chào mừng', 'Xin chào ' + user.name);}}Hạn chế:
WelcomeService mà không gửi email thật là rất khó.WelcomeService.class WelcomeService {constructor(mailer) {this.mailer = mailer; // được "tiêm" vào}welcome(user) {this.mailer.send(user.email, 'Chào mừng', 'Xin chào ' + user.name);}}// Dùng thậtconst service = new WelcomeService(new SmtpMailer());// Dùng trong test: bản giả ghi lại những gì được gửiclass FakeMailer {constructor() { this.sent = []; }send(to, subject, body) { this.sent.push({ to, subject, body }); }}const fake = new FakeMailer();new WelcomeService(fake).welcome({ name: 'An', email: 'an@example.com' });console.log(fake.sent.length); // 1console.log(fake.sent[0].to); // an@example.comChỉ thay đổi một chút: truyền mailer qua constructor thay vì tự tạo. Nhờ vậy WelcomeService không còn biết (và không quan tâm) mailer cụ thể là gì, miễn là nó có phương thức send. Đó chính là dependency injection.
// Python: tiêm qua tham số hàmdef create_report(orders, clock):return {"generated_at": clock(), "count": len(orders)}# Dùng thậtfrom datetime import datetimecreate_report(orders, datetime.now)# Trong test: thời gian cố địnhdef test_create_report():report = create_report([1, 2], lambda: "2025-01-01")assert report["generated_at"] == "2025-01-01"Ví dụ này còn giải quyết bài toán quen thuộc: tách thời gian hiện tại ra thành thứ có thể thay thế để test ổn định.
Trong các ngôn ngữ có kiểu tĩnh (Java, C#, TypeScript, PHP), bạn nên phụ thuộc vào interface thay vì class cụ thể:
interface Mailer {send(to: string, subject: string, body: string): void;}class WelcomeService {constructor(private mailer: Mailer) {}welcome(user: { name: string; email: string }) {this.mailer.send(user.email, 'Chào mừng', 'Xin chào ' + user.name);}}Mọi class nào đáp ứng Mailer đều dùng được: bản SMTP, bản dịch vụ email bên thứ ba, bản giả dùng trong test.
Khi ứng dụng lớn dần, việc tạo thủ công chuỗi phụ thuộc (A cần B, B cần C và D...) trở nên rườm rà. Nhiều framework có DI container tự động dựng đối tượng và cung cấp phụ thuộc: Spring (Java), Laravel (PHP), NestJS, ASP.NET Core... Bạn khai báo cách tạo, container lo phần còn lại.
Tuy nhiên, hãy hiểu nguyên lý trước khi dùng container. Với dự án nhỏ, truyền tay qua constructor tại một nơi duy nhất (thường là điểm khởi động ứng dụng, gọi là composition root) là đủ và dễ hiểu.
DI không phải kỹ thuật cao siêu, nó chỉ là việc "đừng tự tạo những thứ bạn cần, hãy để người khác đưa cho bạn". Thay đổi nhỏ ấy giúp code dễ kiểm thử, dễ bảo trì và linh hoạt hơn hẳn. Hãy thử áp dụng ngay với một class đang gọi trực tiếp database hay dịch vụ ngoài trong dự án của bạn.
Để học thiết kế phần mềm bài bản hơn, bạn có thể xem các khóa học trên codebem.com.
Nguyễn Đình Cường
0 comment