Reading Time: 8 minutes

Trong quá trình phát triển phần mềm bằng Java, việc xử lý các phụ thuộc giữa các thành phần là điều không thể tránh khỏi — và đôi khi, nó khiến mã nguồn trở nên rối rắm, khó kiểm soát. Đó là lúc Dependency Injection (DI) phát huy tác dụng. Đây không chỉ là một kỹ thuật thiết kế phần mềm, mà còn là một tư duy giúp bạn tách biệt các mối quan hệ phức tạp trong hệ thống, từ đó nâng cao tính linh hoạt, dễ kiểm thử và dễ bảo trì. Bằng cách chuyển việc khởi tạo đối tượng từ thời điểm biên dịch sang thời điểm chạy, DI giúp bạn viết mã “sạch” hơn, chuyên nghiệp hơn, và quan trọng nhất là dễ mở rộng theo thời gian.

Dependency Injection trong Java

Dependency Injection có thể khó hiểu nếu chỉ tiếp cận qua lý thuyết, vì vậy hãy bắt đầu bằng một ví dụ đơn giản và sau đó áp dụng mô hình thiết kế này để đạt được sự tách biệt rõ ràng giữa các thành phần, cũng như khả năng mở rộng ứng dụng. Giả sử chúng ta sử dụng EmailService để gửi email. Thông thường, cách triển khai sẽ như sau:

package com.journaldev.java.legacy;

public class EmailService {

	public void sendEmail(String message, String receiver){
		//logic to send email
		System.out.println("Email sent to "+receiver+ " with Message="+message);
	}
}

Lớp EmailService chứa logic riêng gửi email tới người nhận. Mã nguồn của ứng dụng như sau:

package com.journaldev.java.legacy;

public class MyApplication {

	private EmailService email = new EmailService();

	public void processMessages(String msg, String rec){
		//do some msg validation, manipulation logic etc
		this.email.sendEmail(msg, rec);
	}
}

Lớp MyApplication sẽ sử dụng EmailService để gửi email:

package com.journaldev.java.legacy;

public class MyLegacyTest {

	public static void main(String[] args) {
		MyApplication app = new MyApplication();
		app.processMessages("Hi Pankaj", "pankaj@abc.com");
	}

}

Thoạt nhìn, cách triển khai trên có vẻ không có vấn đề gì, nhưng thực tế có một số hạn chế nhất định:

  • Phụ thuộc mã hóa cứng: Lớp MyApplication chịu trách nhiệm khởi tạo EmailService và sử dụng nó. Điều này làm cho mã nguồn khó thay đổi và dẫn đến sự phụ thuộc mã hóa cứng. Nếu chúng ta muốn chuyển sang một số dịch vụ email nâng cao khác trong tương lai, sẽ cần phải thay đổi mã trong lớp MyApplication. Kết quả khiến ứng dụng của chúng ta khó mở rộng và nếu dịch vụ email được sử dụng trong nhiều lớp thì điều đó sẽ còn khó hơn nữa.
  • Giảm tính mở rộng: Nếu chúng ta muốn mở rộng ứng dụng của mình để cung cấp tính năng nhắn tin bổ sung, chẳng hạn như tin nhắn SMS hoặc Facebook thì chúng ta sẽ cần phải viết một ứng dụng khác cho tính năng đó. Bởi nó liên quan đến việc thay đổi mã trong các lớp ứng dụng và trong các lớp máy khách khác.
  • Khó kiểm thử: Do MyApplication đang trực tiếp khởi tạo EmailService, không có cách nào chúng ta có thể mô phỏng các đối tượng này trong các lớp kiểm thử của mình.

Nhiều dev còn thường bỏ tạo phiên bản dịch vụ email khỏi lớp MyApplication bằng cách có một hàm tạo yêu cầu dịch vụ email làm đối số.

package com.journaldev.java.legacy;

public class MyApplication {

	private EmailService email = null;

	public MyApplication(EmailService svc){
		this.email=svc;
	}

	public void processMessages(String msg, String rec){
		//do some msg validation, manipulation logic etc
		this.email.sendEmail(msg, rec);
	}
}

Tuy nhiên, thiết kế này là không tối ưu vì trong trường hợp này ta đang yêu cầu các ứng dụng khách hoặc các lớp kiểm tra khởi tạo dịch vụ email. Giờ hãy xem cách có thể áp dụng mô hình DI để giải quyết tất cả các vấn đề trên. Dependency Injection trong Java yêu cầu ít nhất 3 thành phần:

  • Thành phần dịch vụ: Các dịch vụ nên được thiết kế dựa trên các lớp cơ sở hoặc giao diện ứng dụng để định nghĩa hành vi dịch vụ.
  • Thành phần khách hàng: Viết code dựa trên giao diện dịch vụ.
  • Lớp injector: Khởi tạo các dịch vụ và cung cấp cho thành phần khách hàng.

Các thành phần dịch vụ DI trong Java

Trong trường hợp này, chúng ta có thể sử dụng MessageService để khai báo triển khai dịch vụ.

package com.journaldev.java.dependencyinjection.service;

public interface MessageService {

	void sendMessage(String msg, String rec);
}

Giả sử có dịch vụ Email và SMS triển khai các giao diện trên.

package com.journaldev.java.dependencyinjection.service;

public class EmailServiceImpl implements MessageService {

	@Override
	public void sendMessage(String msg, String rec) {
		//logic to send email
		System.out.println("Email sent to "+rec+ " with Message="+msg);
	}

}
package com.journaldev.java.dependencyinjection.service;

public class SMSServiceImpl implements MessageService {

	@Override
	public void sendMessage(String msg, String rec) {
		//logic to send SMS
		System.out.println("SMS sent to "+rec+ " with Message="+msg);
	}

}

Mô hình Dependency Injection hiện giờ đã sẵn sàng và có thể bắt đầu viết lớp người dùng.

Người dùng dịch vụ Dependency Injection (Thành phần khách hàng)

Không bắt buộc phải có giao diện cơ sở cho các lớp người dùng nhưng cần có một giao diện Consumer đã khai báo hành vi cho lớp người dùng.

package com.journaldev.java.dependencyinjection.consumer;

public interface Consumer {

	void processMessages(String msg, String rec);
}

Lớp người dùng được triển khai như bên dưới.

package com.journaldev.java.dependencyinjection.consumer;

import com.journaldev.java.dependencyinjection.service.MessageService;

public class MyDIApplication implements Consumer{

	private MessageService service;

	public MyDIApplication(MessageService svc){
		this.service=svc;
	}

	@Override
	public void processMessages(String msg, String rec){
		//do some msg validation, manipulation logic etc
		this.service.sendMessage(msg, rec);
	}

}

Lưu ý rằng lớp ứng dụng của chúng ta chỉ sử dụng dịch vụ. Nó không khởi tạo dịch vụ dẫn đến “phân tách các mối quan tâm” tốt hơn. Ngoài ra, việc sử dụng giao diện dịch vụ cho phép chúng ta dễ dàng kiểm tra ứng dụng bằng cách mô phỏng MessageService và liên kết các dịch vụ tại thời điểm chạy thay vì thời gian biên dịch. Bây giờ đã sẵn sàng để viết các lớp injector trong mô hình Dependency Injection, từ đó khởi tạo dịch vụ và cả các lớp người dùng.

Lớp Injector trong Java DI

Hãy nhận diện MessageServiceInjector với phương thức khai báo trả kết quả về lớp Consumer.

package com.journaldev.java.dependencyinjection.injector;

import com.journaldev.java.dependencyinjection.consumer.Consumer;

public interface MessageServiceInjector {

	public Consumer getConsumer();
}

Tại đây với mỗi dịch vụ, chúng ta sẽ phải tạo các lớp injector như bên dưới.

package com.journaldev.java.dependencyinjection.injector;

import com.journaldev.java.dependencyinjection.consumer.Consumer;
import com.journaldev.java.dependencyinjection.consumer.MyDIApplication;
import com.journaldev.java.dependencyinjection.service.EmailServiceImpl;

public class EmailServiceInjector implements MessageServiceInjector {

	@Override
	public Consumer getConsumer() {
		return new MyDIApplication(new EmailServiceImpl());
	}

}
package com.journaldev.java.dependencyinjection.injector;

import com.journaldev.java.dependencyinjection.consumer.Consumer;
import com.journaldev.java.dependencyinjection.consumer.MyDIApplication;
import com.journaldev.java.dependencyinjection.service.SMSServiceImpl;

public class SMSServiceInjector implements MessageServiceInjector {

	@Override
	public Consumer getConsumer() {
		return new MyDIApplication(new SMSServiceImpl());
	}

}

Bây giờ chúng ta hãy quan sát các ứng dụng khách sẽ sử dụng như thế nào với một chương trình đơn giản.

package com.journaldev.java.dependencyinjection.test;

import com.journaldev.java.dependencyinjection.consumer.Consumer;
import com.journaldev.java.dependencyinjection.injector.EmailServiceInjector;
import com.journaldev.java.dependencyinjection.injector.MessageServiceInjector;
import com.journaldev.java.dependencyinjection.injector.SMSServiceInjector;

public class MyMessageDITest {

	public static void main(String[] args) {
		String msg = "Hi Pankaj";
		String email = "pankaj@abc.com";
		String phone = "4088888888";
		MessageServiceInjector injector = null;
		Consumer app = null;

		//Send email
		injector = new EmailServiceInjector();
		app = injector.getConsumer();
		app.processMessages(msg, email);

		//Send SMS
		injector = new SMSServiceInjector();
		app = injector.getConsumer();
		app.processMessages(msg, phone);
	}

}

Như bạn có thể thấy, các lớp ứng dụng chỉ chịu trách nhiệm sử dụng dịch vụ. Các lớp dịch vụ được tạo ra trong injector. Ngoài ra, nếu chúng ta phải mở rộng thêm ứng dụng của mình để cho phép nhắn tin trên Facebook, chúng ta sẽ chỉ phải viết các lớp dịch vụ và lớp injector.

Vì vậy, việc triển khai dependency injection đã giải quyết được vấn đề với các lớp phụ thuộc (dependency) được mã hóa cứng và giúp cho ứng dụng linh hoạt và dễ mở rộng hơn. Giờ thì chúng ta có thể dễ dàng kiểm tra lớp ứng dụng của mình như thế nào bằng cách mô phỏng các lớp injector và các lớp dịch vụ.

Kiểm thử Dependency Injection bằng JUnit

package com.journaldev.java.dependencyinjection.test;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import com.journaldev.java.dependencyinjection.consumer.Consumer;
import com.journaldev.java.dependencyinjection.consumer.MyDIApplication;
import com.journaldev.java.dependencyinjection.injector.MessageServiceInjector;
import com.journaldev.java.dependencyinjection.service.MessageService;

public class MyDIApplicationJUnitTest {

	private MessageServiceInjector injector;
	@Before
	public void setUp(){
		//mock the injector with anonymous class
		injector = new MessageServiceInjector() {

			@Override
			public Consumer getConsumer() {
				//mock the message service
				return new MyDIApplication(new MessageService() {

					@Override
					public void sendMessage(String msg, String rec) {
						System.out.println("Mock Message Service implementation");

					}
				});
			}
		};
	}

	@Test
	public void test() {
		Consumer consumer = injector.getConsumer();
		consumer.processMessages("Hi Pankaj", "pankaj@abc.com");
	}

	@After
	public void tear(){
		injector = null;
	}

}

Tôi đang sử dụng các lớp ẩn danh để mô phỏng các lớp injector và lớp service, từ đó kiểm tra thiết bị của mình một cách dễ dàng. Tôi sử dụng JUnit 4 cho lớp kiểm tra ở trên, vì vậy hãy đảm bảo rằng nó nằm trong đường dẫn dự án của bạn.

Chúng tôi sử dụng các hàm tạo để inject các dependency trong các lớp ứng dụng, một cách khác là sử dụng phương thức setter để “tiêm” các lớp phụ thuộc (dependency) trong các lớp ứng dụng. Đối với phương thức setter, lớp ứng dụng của chúng tôi sẽ được triển khai như bên dưới.

package com.journaldev.java.dependencyinjection.consumer;

import com.journaldev.java.dependencyinjection.service.MessageService;

public class MyDIApplication implements Consumer{

	private MessageService service;

	public MyDIApplication(){}

	//setter dependency injection
	public void setService(MessageService service) {
		this.service = service;
	}

	@Override
	public void processMessages(String msg, String rec){
		//do some msg validation, manipulation logic etc
		this.service.sendMessage(msg, rec);
	}

}
package com.journaldev.java.dependencyinjection.injector;

import com.journaldev.java.dependencyinjection.consumer.Consumer;
import com.journaldev.java.dependencyinjection.consumer.MyDIApplication;
import com.journaldev.java.dependencyinjection.service.EmailServiceImpl;

public class EmailServiceInjector implements MessageServiceInjector {

	@Override
	public Consumer getConsumer() {
		MyDIApplication app = new MyDIApplication();
		app.setService(new EmailServiceImpl());
		return app;
	}

}

Một trong những ví dụ tốt nhất về phương thức Setter Injection là giao diện Struts2 Servlet API Aware. Việc sử dụng các hàm tạo (Constructor Injection) hay dựa trên setter là quyết định thiết kế phụ thuộc vào yêu cầu của bạn.

Ví dụ, nếu ứng dụng của tôi không thể hoạt động nếu không có lớp dịch vụ thì tôi sẽ thích Constructor Injection hoặc ngược lại, tôi sẽ sử dụng Setter Injection để chỉ sử dụng khi thực sự cần thiết.

Dependency Injection trong Java là cách để đạt được Inversion of control (IoC) trong ứng dụng của chúng ta bằng cách di chuyển các đối tượng liên kết từ thời gian biên dịch sang thời gian chạy. Chúng ta cũng có thể đạt được IoC thông qua các kỹ thuật như Factory Pattern, Template Method Design Pattern, Strategy Pattern và Service Locator pattern.

Một số thư viện và Framework như Spring Dependency Injection, Google GuiceJava EE CDI có thể hỗ trợ triển khai DI thông qua việc sử dụng Java Reflection API và java annotation. Tất cả những gì chúng ta cần là chú thích trường, phương thức constructor hoặc setter và xác định cấu hình chúng trong các tệp xml hoặc lớp cấu hình.

Lợi ích và hạn chế của Dependency Injection

Lợi ích:

  • Phân tách/Chia nhỏ các thành phần của ứng dụng thành các đơn vị độc lập giúp việc kiểm tra dễ dàng hơn
  • Giảm boilerplate code vì việc tạo ra các biến phụ thuộc đã được injector thực hiện
  • Kích hoạt những kết nối cần thiết chặt chẽ hơn giúp ứng dụng dễ dàng mở rộng
  • Kiểm tra Unit dễ dàng hơn với các đối tượng mô phỏng

Hạn chế:

  • Nếu lạm dụng DI, nó có thể dẫn đến các vấn đề khó bảo trì vì tác động của các thay đổi được biết đến khi chạy chương trình.
  • Khó phát hiện lỗi biên dịch trong thời gian chạy chương trình vì Dependency injection tiềm ẩn các thành phần phụ thuộc.

Kết luận

Bài viết trên là tổng hợp tất cả về kỹ thuật phần mềm Dependency Injection trong Java. Đây là một kỹ thuật quan trọng trong lập trình hướng đối tượng, đóng vai trò tối ưu hóa việc quản lý và kiểm soát các phụ thuộc (dependencies) trong hệ thống. Bằng cách tách biệt các thành phần và cung cấp chúng một cách linh hoạt, DI giúp mã nguồn trở nên dễ bảo trì, mở rộng và kiểm thử hơn.

Tuy nhiên, việc áp dụng DI không chỉ đơn thuần là tuân theo một mô hình có sẵn mà đòi hỏi sự cân nhắc cẩn thận để đảm bảo rằng nó thực sự phù hợp với yêu cầu của dự án. Vì vậy, trước khi quyết định sử dụng DI, bạn cần đánh giá kỹ lưỡng những lợi ích và hạn chế để có thể tận dụng tối đa sức mạnh của kỹ thuật này trong phát triển phần mềm.

0 Bình luận

Đăng nhập để thảo luận

CyStack blog

Mẹo, tin tức, hướng dẫn và các best practice độc quyền của CyStack

Đăng ký nhận bản tin của chúng tôi

Hãy trở thành người nhận được các nội dung hữu ích của CyStack sớm nhất

Xem chính sách của chúng tôi Chính sách bảo mật.