DI - IoC và cách chúng ứng dụng trong Spring Framework
Trong thế giới phát triển phần mềm hiện đại, việc tạo ra mã nguồn sạch, dễ bảo trì và dễ mở rộng là một trong những mục tiêu quan trọng nhất của các lập trình viên. Một trong những công cụ mạnh mẽ giúp chúng ta đạt được mục tiêu này chính là Dependency Injection (DI) và Inversion of Control (IoC) – hai khái niệm cốt lõi trong Spring, framework Java phổ biến nhất hiện nay. Chúng ta hãy cùng nhau tìm hiểu về chúng trong bài blog này.
I. Dependency Injection
1. Khái niệm
Dependency injection dịch thô ra sẽ là tiêm sự phụ thuộc, nó được phát biểu là một kỹ thuật lập trình trong đó dependencies không được khởi tạo trong đối tượng mà nó inject từ bên ngoài thông qua các injector. Các injector có thể là Constructor Injection (tiêm qua hàm khởi tạo), Setter Injection (tiêm qua phương thức setter), hoặc Field Injection (tiêm trực tiếp vào các trường). Việc sử dụng Dependency Injection giúp tăng tính mô-đun và khả năng kiểm thử của mã nguồn bằng cách giảm sự phụ thuộc trực tiếp giữa các lớp.
Nếu các bạn cảm thấy khái niệm trên khó hiểu chúng ta hãy cũng xem 2 ví dụ dưới đây:
public class MyService {
private Repository repository;
public MyService() {
// Tạo đối tượng Repository trực tiếp trong lớp
this.repository = new Repository();
}
}public class MyService {
private final Repository repository;
// inject đối tượng thông qua contructor
public MyService(Repository repository) {
this.repository = repository;
}
}Trong 2 ví dụ trên ta có thể dễ dàng nhìn thấy với việc sử dụng như cách thứ nhất class MyService phụ thuộc chặt chẽ vào Repository. Nếu cần thấy đổi hoặc mở rộng Repository ta sẽ cần phải trực tiếp vào class MyService để sửa từ đó code chúng ta sẽ khó mở rộng và không linh hoạt. Ngược lại ở cách thứ hai MyService chỉ phụ thuộc vào interface hoặc abstract class Repository, từ đấy ta có thể dễ dàng thay thế hoặc thay đổi triển khai.
2. Các loại Injector
2.1 Contructor injection
Trường hợp sử dụng:
Dependencies bắt buộc: Khi một lớp không thể hoạt động mà không có dependencies, bạn nên sử dụng constructor injection. Điều này đảm bảo rằng tất cả các dependencies cần thiết được cung cấp khi đối tượng được khởi tạo.
Tính bất biến: Constructor injection cho phép bạn đánh dấu các dependencies là final, giúp đảm bảo rằng dependencies này không bị thay đổi sau khi đối tượng được tạo. Điều này làm tăng tính an toàn và ổn định của mã.
Kiểm thử dễ dàng: Các đối tượng có thể được kiểm thử một cách dễ dàng hơn với constructor injection, vì tất cả các dependencies có thể được cung cấp trực tiếp trong khi khởi tạo đối tượng trong các bài kiểm thử đơn vị (unit test).
Bài toán: Quản lý Đặt phòng Khách sạn
Giả sử bạn đang xây dựng một ứng dụng quản lý đặt phòng khách sạn. Một lớp BookingService cần phải có các dependencies bắt buộc là RoomRepository (để truy xuất thông tin phòng) và PaymentProcessor (để xử lý thanh toán cho đặt phòng). Vì BookingService không thể hoạt động nếu thiếu một trong hai dependencies này, chúng ta nên sử dụng Constructor Injection để đảm bảo rằng cả hai đều được cung cấp khi BookingService được khởi tạo.
@Component
public class BookingService {
private final RoomRepository roomRepository;
private final PaymentProcessor paymentProcessor;
@Autowired
public BookingService(
RoomRepository roomRepository,
PaymentProcessor paymentProcessor
) {
this.roomRepository = roomRepository;
this.paymentProcessor = paymentProcessor;
}
}
2.2 Setter Injection
Trường hợp sử dụng:
Dependencies tùy chọn hoặc có thể thay đổi: Sử dụng setter injection khi dependencies không bắt buộc hoặc có thể thay đổi sau khi đối tượng đã được khởi tạo. Điều này cho phép linh hoạt trong việc thay đổi dependencies mà không cần phải khởi tạo lại đối tượng.
Cấu hình sau khi khởi tạo: Nếu bạn cần cấu hình đối tượng sau khi nó đã được khởi tạo với các giá trị mặc định, setter injection là một lựa chọn tốt.
Bean vòng tròn (Circular Dependencies): Trong trường hợp có sự phụ thuộc vòng tròn giữa các beans, setter injection có thể giúp giải quyết vấn đề này, vì Spring có thể khởi tạo các beans trước, sau đó thiết lập các dependencies thông qua các setter.
Bài toán: Quản lý Gửi Email Quảng cáo
Trong một hệ thống quản lý marketing, bạn có một lớp EmailMarketingService chịu trách nhiệm gửi email quảng cáo đến khách hàng. EmailMarketingService sử dụng EmailSender để thực sự gửi email. Tuy nhiên, việc gửi email có thể được cấu hình lại (ví dụ: thay đổi SMTP server hoặc địa chỉ email gửi đi) tùy thuộc vào chiến dịch marketing. Trong trường hợp này, sử dụng Setter Injection cho EmailSender là hợp lý vì nó có thể được thay đổi sau khi khởi tạo đối tượng EmailMarketingService.
public class EmailService {
private EmailSender emailSender;
public void setEmailSender(EmailSender emailSender) {
this.emailSender = emailSender;
}
}
2.3 Field Injection
Trường hợp sử dụng:
Đơn giản hóa mã: Field injection thường được sử dụng trong các lớp đơn giản hoặc khi bạn muốn viết mã ngắn gọn mà không cần thêm constructor hoặc setter. Điều này thường thấy trong các ứng dụng mẫu hoặc ứng dụng đơn giản.
Frameworks và công cụ hỗ trợ mạnh: Trong một số trường hợp, đặc biệt là khi sử dụng các framework hỗ trợ injection mạnh như Spring Boot, field injection có thể được sử dụng để đơn giản hóa việc wiring dependencies.
Bài toán: Dịch vụ Ghi nhật ký (Logging Service)
Trong một hệ thống, bạn có một lớp LoggingService để ghi nhật ký hoạt động của hệ thống. LoggingService sử dụng Logger để ghi lại các sự kiện vào file log. Đây là một lớp đơn giản không đòi hỏi cấu hình phức tạp hoặc kiểm thử mạnh mẽ, vì vậy bạn có thể sử dụng Field Injection để đơn giản hóa mã.
public class LoggingService {
@Autowired
private Logger logger;
public void log(String message) {
logger.info(message);
}
}
II. Inversion of Control
1. Khái niệm
IoC (Inversion of Control) là một nguyên tắc lập trình trong việc quản lý sự phụ thuộc giữa các thành phần trong một hệ thống phần mềm. Thay vì các thành phần tự mình tạo ra và quản lý sự phụ thuộc của chúng, IoC chuyển trách nhiệm này cho một thực thể bên ngoài được gọi là Container hay Framework. Nếu bạn còn thấy khó hiểu thì bạn có thể hiểu nôm na như việc chuyển phòng trọ chẳng hạn, thay vì chúng ta phải tự dọn phòng, quét dọn, lau chùi, đóng gói đồ đạc,… chúng ta sẽ thuê một bên thứ ba làm việc đó cho mình. Nhiệm vụ của mình chỉ là sang phòng trọ mới và chờ người ta mang đồ đến.
Trong Spring IoC được triển khai thông qua DI. Ta cùng xem lại 2 ví dụ ở trên mình đã nêu
public class MyService {
private Repository repository;
public MyService() {
// Tạo đối tượng Repository trực tiếp trong lớp
this.repository = new Repository();
}
}public class MyService {
private final Repository repository;
// inject đối tượng thông qua contructor
public MyService(Repository repository) {
this.repository = repository;
}
}Đối với ví dụ đầu tiên chúng ta đã khởi tạo một đối tượng Repository mới từ đó ta có thể sử dụng các phương thức mà đối tượng Repository cung cấp. Tuy nhiên với ví dụ thứ hai tại sao chúng ta không khởi tạo đối tượng mới mà chúng ta vẫn có thể sử dụng các phương thức của tôi tượng Repository. Hãy quay lại định nghĩa của IoC ở trên “Thay vì các thành phần tự mình tạo ra và quản lý sự phụ thuộc của chúng, IoC chuyển trách nhiệm này cho một thực thể bên ngoài được gọi là Container hay Framework”. Vậy tức là đã có một bên thứ ba đứng ra quản lí tạo mới đối tượng Repository và tự inject vào class MyService đúng không nào. Đối với Spring Framework bên đứng ra quản lí các dependencies trên được gọi là IoC Container. Ta cùng xem tiếp về IoC Container bên dưới đây!
2. IoC Container
IoC container là một cơ chế giúp các đối tượng trong ứng dụng không cần tự khởi tạo các phụ thuộc của mình. IoC Container sẽ tạo ra các đối tượng, nối chúng lại với nhau, cấu hình chúng, và quản lý vòng đời của chúng từ khi tạo ra đến khi bị hủy. IoC Container sử dụng DI để quản lý các thành phần tạo nên một ứng dụng. Những đối tượng này được gọi là Spring Bean.
Trong IoC Container có hai loại chính:
2.1 BeanFactory
BeanFactory là một IoC container đơn giản và cơ bản trong Spring Framework. Nó cung cấp các khả năng quản lý các bean (các đối tượng Java), bao gồm việc khởi tạo và xử lý phụ thuộc.
BeanFactory khởi tạo bean khi được yêu cầu (lazy loading). Điều này có nghĩa là bean chỉ được khởi tạo khi nó thực sự cần thiết, giúp tiết kiệm tài nguyên, đặc biệt trong các ứng dụng có nhiều bean mà không phải tất cả đều được sử dụng cùng lúc.
Ưu điểm:
Phù hợp với các ứng dụng nhỏ, nơi bạn không cần quản lý phức tạp.
Hiệu quả với việc quản lý tài nguyên vì bean chỉ được tạo khi cần thiết.
Hạn chế:
Không hỗ trợ một số tính năng cao cấp như xử lý các event hoặc quản lý các lifecycle hook (callback method).
2.2 ApplicationContext
ApplicationContext là một loại IoC container tiên tiến hơn. Nó mở rộng BeanFactory và cung cấp thêm nhiều tính năng bổ sung như event propagation, hỗ trợ internationalization (i18n), quản lý các event lifecycle, và khả năng tương tác với các resource file.
ApplicationContext khởi tạo tất cả các bean tại thời điểm container được load (eager loading), tức là các bean sẽ được khởi tạo ngay lập tức khi container khởi động, thay vì đợi đến lúc được sử dụng như trong BeanFactory.
Ưu điểm:
Hỗ trợ quản lý lifecycle cho các bean thông qua các callback method như init và destroy.
Cho phép lắng nghe và phát các event trong ứng dụng, giúp ứng dụng có thể phản hồi nhanh chóng khi có sự thay đổi.
Hạn chế:
Khởi tạo các bean ngay khi ứng dụng bắt đầu (eager loading), có thể làm tăng thời gian khởi động nếu số lượng bean lớn.
2.3 Sự khác biệt chính
BeanFactory sử dụng lazy loading, chỉ khởi tạo bean khi cần, còn ApplicationContext sử dụng eager loading, khởi tạo tất cả các bean ngay khi container khởi chạy.
ApplicationContext hỗ trợ nhiều tính năng hơn như quản lý event, resource, và lifecycle, trong khi BeanFactory chỉ cung cấp các tính năng cơ bản.
Tùy vào nhu cầu và độ phức tạp của ứng dụng, bạn có thể chọn loại IoC container phù hợp. Trong các ứng dụng Spring hiện đại, ApplicationContext được sử dụng phổ biến hơn do tính năng phong phú và khả năng mở rộng tốt hơn.
Tổng kết lại, việc áp dụng Dependency Injection (DI) và Inversion of Control (IoC) giúp phát triển phần mềm linh hoạt, dễ bảo trì và mở rộng. IoC container trong Spring là công cụ mạnh mẽ quản lý phụ thuộc, đảm bảo ứng dụng dễ dàng thay đổi khi cần thiết..Trong bài viết tiếp theo, chúng ta sẽ cùng khám phá sâu hơn về Bean trong Spring, cách Bean được quản lý, cấu hình, và các phương pháp tiếp cận để tối ưu hóa việc sử dụng chúng.

