Java Dependency Injection – DI Design Pattern Example Tutorial

Java Dependency Injection design pattern cho phép chúng ta loại từ việc phụ thuộc vào hard code và làm cho app trở nên linh hoạt trong việc tích hợp, có khả năng mở rộng và maintain. Chúng ta có thể sử dụng dependency injection trong java để di chuyển các dependency resolution từ compile-time tới runtime.

Java Dependency injection khá là khó nắm bắt với lý thuyết, vì vậy tôi sẽ lấy một ví dụ đơn giản và sau đó chúng ta sẽ thấy làm thế nào để sử dụng dependency injection pattern để đạt được những điều đã nói ở trên trong ứng dụng.

Giả sử chúng ta có một ứng dụng sử dụng EmailService để gửi email. Thông thường, ta sẽ làm 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);
	}
}

EmailService class chứa các logic để send mail tới người nhận. App sẽ sử dụng class này như đoạn code dưới đây.

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);
	}
}

Client application sẽ sử dụng class MyApplication để gửi email như sau.

package com.journaldev.java.legacy;

public class MyLegacyTest {

	public static void main(String[] args) {
		MyApplication app = new MyApplication();
		app.processMessages("Hi Pankaj", "[email protected]");
	}

}

Nhìn vào thì chúng ta thấy ko có vấn đề gì với cách thực thi ở bên trên đâu nhỉ ? Mọi thứ có vẻ đều ổn. Tuy nhiên, cách làm này có một số giới hạn

  • Class MyApplication có trách nhiệm khởi tạo service email và sau đó sử dụng nó. Điều này dẫn đến sự phụ thuộc trong code. Giả sử nếu chúng ta muốn chuyển sang một số service email khác ngon hơn trong tương lai, ta sẽ phải thay đổi code cả trong trong lớp MyApplication. Điều này làm cho ứng dụng khó mở rộng và nếu service email được sử dụng trong nhiều class thì điều đó thậm chí còn mất thời gian và khó hơn nhiều.

  • Hoặc nếu chúng ta muốn mở rộng ứng dụng để nâng cấp tính năng nhắn tin, chẳng hạn như tin nhắn SMS hoặc Facebook thì sẽ cần phải viết lại một application khác cho điều đó. Điều này sẽ bao gồm cả việc code một class mới và thay đổi code phía client class để sử dụng application mới này.

  • Việc tiến hành làm unit test cùng sẽ rất phức tạp vì application đang tạo ra các instance email service một cách trực tiếp. Không có cách nào chúng ta có thể giả lập các objects trong class test. Một số người có thể nói rằng chúng ta sẽ loại bỏ các việc tạo instance email service trong MyApplication bằng sử dụng constructor có đầu vào là email service. Ví dụ như sau :

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);
	}
}

Nhưng trong trường hợp này, chúng ta lại đang yêu cầu client application hoặc test class phải khởi tạo email service. Rõ ràng đây không phải là một quyết định đúng đắn xét về mặt thiết kế. Bây giờ hãy xem chúng ta có thể sử dụng java dependency injection pattern vào để giải quyết các vấn đề trên như thế nào. java dependency injection pattern sẽ yêu cầu các rule như sau :

  • Các thành phần của service nên được thiết kế dựa trên base class hoặc interface. Tốt nhất là nên sử dụng interface hoặc lớp trừu trượng - định nghĩa rõ ràng các service.
  • Consumer classes nên được viết dưới dạng service interface.
  • Injector classes sẽ khởi tạo service sau đó mới tới consumer class.

Java Dependency Injection – Service Components

Trong trường hợp này chúng ta sẽ có một interface MessageService.

package com.journaldev.java.dependencyinjection.service;

public interface MessageService {

	void sendMessage(String msg, String rec);
}

Chúng ta có service Email và SMS implement interface ở 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);
	}

}

Dependency injection java services đã sẵn sàng, tiếp đến chúng ta sẽ tiếp tục với các consumer classes.

Java Dependency Injection – Service Consumer

Mặc dù ko nhất thiết phải có base interface cho consumer class nhưng tôi đã tạo một cái như thế dưới đây cho nó.

package com.journaldev.java.dependencyinjection.consumer;

public interface Consumer {

	void processMessages(String msg, String rec);
}

Consumer Class sẽ như dưới đây.

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);
	}

}

Chú ý rằng class application đang sử dụng service. Nó sẽ ko khới tạo service mà có việc tách biệt các chức năng một cách tốt hơn. Thay vì đó việc sử dụng service interface sẽ cho phép chúng ta viết unit test dễ dàng hơn bằn việc tạo các MessageService và bind service tại thời điểm runtime hơn là compile time. Bây giờ chúng ta đã sẵn sàng để viết class dependency injector khởi tạo service và consumer class.

Java Dependency Injection – Injectors Classes

Đầu tiên chúng ta sẽ có interface MessageServiceInjector với method khai báo trả về Consumer class.

package com.journaldev.java.dependencyinjection.injector;

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

public interface MessageServiceInjector {

	public Consumer getConsumer();
}

Ở đây, mọi service, chúng ta sẽ phải tạo class injector như sau.

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ờ hãy xem client applications sẽ sử dụng application như thế nào trong một chương trình đơn giản nhé.

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 = "[email protected]";
		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 thấy, class application chịu trách nhiệm cho việc sử dụng service. Class service được tạo trong injector. Và nếu bạn phải mở rộng ứng dụng cho phép facebook messaging chẳng hạn, chúng ta sẽ chỉ cần phải viết class Service cho nó và một class injector thôi.

Ở đây bạn sẽ thấy việc áp dụng dependency injection sẽ giải quyết được vấn đề phụ thuộc vào hard code và giúp ứng dụng trở nên flexible và dễ dàng cho maintain mở rộng hơn ko ? Tiếp hãy xem việc unit test sẽ dễ dang như thế nào bằng cách mock injector và service class.

Java Dependency Injection – JUnit Test Case with Mock Injector and Service

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", "[email protected]");
	}
	
	@After
	public void tear(){
		injector = null;
	}

}

Như bạn đã thấy mình sửu dụng một class anonymous để mock injector và class service và đã có thể dễ dàng test method của ứng dụng. Ở trên mình sử dụng JUnit 4, cho nên hãy đảm bảo nó sẽ nằm trong build path của project của bạn nếu bạn muốn chạy thử đoạn code trên.

Chúng ta đã sử dụng constructors để inject dependencies vào trong class application, một cách khác nữa là sử dụng setter method. Với cách thứ 2 này, application class sẽ như dưới đây.

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 ví dụ điển hành của setter dependency injection chính là interface Struts2 Servlet API Aware.

Việc sử dụng Injection dependency bằng Constructor hay setter dựa vào quyết định design và yêu cầu dự án của bạn. Ví dụ, nếu ứng dụng của bạn ko thể làm việc được mà ko sử dụng bất ký class service nào thì tốt nhất bạn nên dùng DI với constructor, và chỉ sử dụng DI dựa trên setter method khi thực sự cần thiết

Dependency Injection trong Java sẽ giúp bạn có được Inversion of control (IoC) trong ứng dụng bằng cách di chuyển các đối tượng từ compile time sang runtimr. Chúng ta có thể có được IoC thông qua các mô hình như Factory Pattern, Template Method Design Pattern, Pattern Pattern và Service Locator.

Spring Dependency Injection, framework Google Guice và Java EE CDI sẽ tạo thuận lợi cho quá trình dependency injection thôn qua việc sử dụng Java Reflection API và java annotations. Tất cả những gì chúng ta cần là chú thích field, constructor hay setter và cấu hình chúng trong các file cấu hình xml hoặc class.

Benefits of Java Dependency Injection

Một vài lợi ích của việc sử dụng Dependency Injection trong Java :

  • Tách biệt chức năng, khái niệm
  • Giảm lượng Code trùng lặp trong các application class bởi vì tất cả các công việc để khởi tạo dependencies được xử lý bởi the injector component.
  • Việc các component có thể cấu hình giúp ứng dụng dễ dàng mở rộng
  • Dễ dàng cho Unit test

Disadvantages of Java Dependency Injection

Tất nhiên, Java Dependency injection cũng sẽ có một vài nhược điểm

  • Nếu quá lạm dụng, nó có thể dẫn đến các vấn đề khi bảo trì bởi vì hiệu quả của những thay đổi chỉ được biết đến khi runtime.
  • Dependency injection trong java ẩn các service class dependency, việc này có thể dẫn tới runtime error tại lúc compile.

Nguồn

http://www.journaldev.com/2394/java-dependency-injection-design-pattern-example-tutorial


All Rights Reserved