Dependency Injection là gì? Nguyên tắc tiêm phụ thuộc trong lập trình

Dependency Injection (DI) là một nguyên tắc thiết kế phần mềm trong đó các đối tượng không tự tạo ra phụ thuộc của chúng, mà nhận phụ thuộc từ bên ngoài. Nhờ đó, các lớp trở nên độc lập, dễ kiểm thử và dễ thay thế. DI là nền tảng của Spring Framework, .NET Core và hầu hết framework hiện đại. Bài viết này giải thích vì sao DI quan trọng, phân biệt constructor injection với setter injection, và chỉ ra những sai lầm thường gặp.

Vấn đề: phụ thuộc cứng

Hãy xét một lớp OrderService cần gửi email thông báo. Cách viết tự nhiên — và sai — là tự khởi tạo lớp gửi mail bên trong:

public class OrderService {
    private SmtpMailer mailer = new SmtpMailer("smtp.example.com");
    public void placeOrder() {
        mailer.send("Đơn hàng đã tạo");
    }
}

Ở đây OrderService gắn chặt với SmtpMailer và với cấu hình SMTP cụ thể. Hệ quả:

  • Không kiểm thử được. Muốn test placeOrder(), bạn phải thật sự gửi email thật — hoặc lộ ra một hack rò rỉ phụ thuộc vào toàn cục.
  • Không thay thế được. Muốn đổi sang nhà cung cấp email khác, phải sửa mã nguồn lớp này.
  • Không tái sử dụng được. Cứ mỗi lần tạo OrderService lại tạo một kết nối SMTP mới, tốn tài nguyên.
Sơ đồ service provider bên ngoài cấp phụ thuộc cho các lớp client

Giải pháp: tiêm phụ thuộc từ bên ngoài

Dependency Injection đảo ngược trách nhiệm: bên nhận (client) chỉ khai báo cần gì, còn bên cung cấp (service provider) quyết định dùng cái gì. Lớp OrderService được viết lại:

public class OrderService {
    private readonly IMailer mailer;
    public OrderService(IMailer mailer) {
        this.mailer = mailer;
    }
    public void placeOrder() {
        mailer.send("Đơn hàng đã tạo");
    }
}

Giờ muốn test, chỉ cần truyền vào một FakeMailer ghi nhận nội dung email. Muốn đổi nhà cung cấp, chỉ thay ở nơi khởi tạo mà không đụng tới logic nghiệp vụ. Đây chính là ý nghĩa của “Inversion of Control” — quyền kiểm soát được đảo chiều.

Sơ đồ ví dụ ứng dụng dùng DI: lớp ứng dụng phụ thuộc vào interface, container cung cấp hiện thực

Ba kiểu tiêm phụ thuội phổ biến

Kiểu Cách thực hiện Đánh giá
Constructor injection Truyền phụ thuộc qua tham số hàm khởi tạo Khuyến nghị — phụ thuộc bắt buộc, rõ ràng, dễ test
Setter injection Gán qua hàm setter Dành cho phụ thuộc tuỳ chọn, đã khởi tạo xong mới set
Field injection Gán trực tiếp vào thuộc tính (@Autowired) Ngắn gọn nhưng ẩn phụ thuộc, khó unit test — nên tránh

Quy tắc thực tế: constructor injection cho mọi phụ thuộc bắt buộc. Nó làm lớp bất biến (immutable), biểu thị đúng điều kiện tồn tại của đối tượng, và giúp trình biên dịch bắt lỗi sớm khi thiếu phụ thuộc.

IoC container: khái niệm và lựa chọn

Container (hay service locator, injector) là thành phần giữ vai trò bên cung cấp. Nó biết cách tạo đối tượng và tự động điều phụ thuộc. Các loại phổ biến:

  • Bản chất có sẵn — Spring @Component + constructor injection; .NET Core built-in DI; Guice; Dagger (compile-time).
  • Convention over configuration — @Service, @Repository trong Spring: container tự nhận diện lớp qua annotation.
  • Annotation-driven — phổ biến trong Java, C#.

Nếu dự án nhỏ, DI thủ công qua constructor thường là đủ và dễ theo dõi hơn — không cần framework nào. Container chỉ đáng dùng khi số lớp và mức độ phụ thuộc đủ lớn để quản lý thủ công trở nên cồng kềnh.

Những sai lầm thường gặp

  • Service locator phủ nhận DI. Gọi container.GetService(IMailer) bên trong lớp nghiệp vụ tạo phụ thuộc ẩn — đúng cái DI cố tránh. Phụ thuộc phải đi qua constructor.
  • Dependency Injection Framework (DIF) để ghi code. Dùng framework DI để xử lý luồng nghiệp vụ tạo mã khó hiểu, khó test. Nó chỉ nên dùng cho dữ liệu đơn giản, không có logic.
  • Phụ thuộc cụ thể thay vì interface. Phụ thuộc vào lớp cụ thể khiến mất khả năng thay thế — đúng thứ DI sinh ra để loại bỏ.
  • Lạm dụng singleton. Đặt mọi thứ là singleton để “tiện” dễ dẫn tới trạng thái chia sẻ và lỗi khó truy.
Sơ đồ lớp UML mô tả ví dụ nghiệp vụ hóa đơn với các interface và lớp triển khai

Kết luận

Dependency Injection không chỉ là kỹ thuật, mà là nguyên tắc thiết kế: đảo ngược hướng phụ thuộc để phần mềm dễ thay đổi, dễ kiểm thử, dễ mở rộng. Với constructor injection làm mặc định, bạn đã nắm được phần lớn lợi ích mà không cần framework phức tạp. Hãy áp dụng nó cho lớp nghiệp vụ trước, để framework DI chỉ là lớp trợ giúp hợp lý ở biên.

Tài nguyên để tìm hiểu thêm

Nếu bạn muốn đi sâu hơn, các tài liệu chính thức sau đây có ví dụ code đầy đủ bằng nhiều ngôn ngữ:

Tôi là một lập trình viên IOS. Code chính là IOS nhưng thỉnnh thoảng vẫn đá sang Android hoặc web. Mặc dù không quá thông thạo nhưng tôi sẽ chia sẻ những kiến thức mà mình đã tìm hiểu, áp dụng qua.

Bài viết liên quan

HyperLogLog là gì? Cấu trúc dữ liệu xác suất đếm phần tử duy nhất

HyperLogLog là gì? Cấu trúc dữ liệu xác suất để ước lượng unique HyperLogLog (HLL) là một cấu trúc dữ liệu xác suất được thiết kế để ước lượng số…

Xem thêm

FastAPI là gì? Framework Python hiện đại để xây dựng API

FastAPI là framework web Python hiện đại dùng để xây dựng API với hiệu năng cao và tự động sinh tài liệu. Bài viết này phân tích kiến trúc, cấu…

Xem thêm

Vite là gì? Dev server siêu tốc cho lập trình viên JavaScript

Vite là công cụ build phía dev server dành cho lập trình viên JavaScript và TypeScript, được Evan You tạo ra sau khi rời dự án Vue.js. Điểm mạnh cốt…

Xem thêm
0 0 đánh giá
Article Rating
Theo dõi
Thông báo của
guest
0 Comments
Cũ nhất
Mới nhất Được bỏ phiếu nhiều nhất