Java – Dependency Injection

Dependency Injection cho phép chúng ta loại bỏ những Dependecy bị hard-code và tạo cho ứng dụng chúng ta gắn kết lỏng lẻo, có khả năng mở rộng và có khả năng bảo trì. Chúng ta có thể thực thi Dependency Injection để chuyển việc giải quyết Dependency khi biên dịch sang thời gian chạy

Dependency Injection thường gây khó hiểu khi đọc lý thuyết, nên chúng ta sẽ cùng nhau tìm hiểu nó thông qua một vài ví dụ đơn giản

Giả sử chúng ta có một ứng dụng sử dụng EmailService để gửi email, thông thường chúng ta sẽ thực hiện như sau

package com.example.java.legacy;

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

Class EmailService nắm giữ logic gửi thông tin email đến địa chỉ email người nhận. Code của ứng dụng sẽ như sau

package com.example.java.legacy;

public class MyApplication {
  private EmailService email = new EmailService();
    public void processMessages(String msg, String rec){
      //logic tính toán, validate msg
      this.email.sendEmail(msg, rec);
    }
}

Code ở phía client sẽ sử dụng MyApplication class để gửi thông tin email

package com.example.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 lần đầu tiên, phần thực hiện code bên trên không có vấn đề gì cả. Nhưng logic code bên trên chắc chắn bị bạn chế

  • MyApplication class có trách nhiệm khởi tạo email service và sử dụng nó. Điều này sẽ dẫn đến việc hard-code cho Dependency, nếu chúng ta thay đổi một vài đặc tính nâng cao của email service trong tương lai, nó sẽ dẫn đến việc cần thay đổi code ở trong MyApplication. Điều này làm cho ứng dụng của chúng ta khó mở rộng và nếu email service được sử dụng trong nhiều class thì nó sẽ còn khó khăn hơn.
  • Nếu bạn muốn mở rộng ứng dụng để cung cấp thêm tính năng cho phần gửi tin nhắn như là SMS hoặc Facebook messager thì chúng ta cần phải viết một ứng dụng mới để thực hiện điều này. Điều này dẫn đến việc cần phải thay đổi code trong các class của  application và client.
  • Việc test các application trở nên rất khó khăn bởi vì application đang tạo email service instance trực tiếp, sẽ không có cách nào để mock các object này trong các class test.

Một giả thiết được đưa ra rằng chúng ta có thể xoá phần tạo instance của email service trong class MyApplication bằng cách truyền nó vào trong constructor như một tham số.

package com.example.java.legacy;

public class MyApplication {

  private EmailService email = null;
	
  public MyApplication(EmailService svc){
    this.email = svc;
  }
	
  public void processMessages(String msg, String rec){
    // thực hiện validate msg, tính toán logic
    this.email.sendEmail(msg, rec);
  }
}

Nhưng trong trường hợp này, các class phía client hoặc trong các class test cần phải thực hiện khởi tạo email service mà đó không phải là quyết định tốt.

Chúng ta hãy cùng xem làm cách nào để Dependency Injection của Java có thể giúp chúng ta giải quyết được vấn đề này với các thực thi bên trên.

Dependency Injection trong Java yêu cầu tối thiểu như sau:

  1. Thành phần Service nên được thiết kế như một class cơ sở hay interface. Tốt hơn hết nó nên là abstract class hoặc interface và sẽ định nghĩa các giao kèo cho các Service.
  2. Các class Consumer nên được viết dựa theo interface của Service
  3. Các class Injector đó sẽ được khởi tạo trong Service và sau đó các class Comsumer.

Thành phần Service

Với trường hợp trên, chúng ta có thể có MessageService dùng để khai báo các giao kèo cho các Service thực thi

package com.example.java.dependencyinjection.service;

public interface MessageService {
  void sendMessage(String msg, String rec);
}

Tiếp theo chúng ta sẽ có email service và SMS service thực thi dựa vào interface bên trên

package com.example.java.dependencyinjection.service;

public class EmailServiceImpl implements MessageService {
  @Override
  public void sendMessage(String msg, String rec) {
    //logic gửi email
    System.out.println("Email sent to " + rec + " with Message=" + msg);
  }
}
package com.example.java.dependencyinjection.service;

public class SMSServiceImpl implements MessageService {
  @Override
  public void sendMessage(String msg, String rec) {
    //logic gửi SMS
    System.out.println("SMS sent to " + rec + " with Message=" + msg);
  }
}

Các class Dependency Injection đã sẵn sàng và bây giờ chúng ta cần viết class Consumer

Service Consumer

Không cần thiết phải có interface hoặc abstract cho class Consumer nhưng chúng ta sẽ sử dụng để tạo giao kèo cho các class thực thi

package com.example.java.dependencyinjection.consumer;

public interface Consumer {
  void processMessages(String msg, String rec);
}

Consumer class sẽ như sau

package com.example.java.dependencyinjection.consumer;

import com.example.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){
    //thực hiện validate msg, tính toán logic...
    this.service.sendMessage(msg, rec);
  }
}

Chú ý rằng class Application chỉ đang sử dụng Service, nó không khởi tạo service. Việc sử dụng Service interface cho phép chúng ta dễ dàng test ứng dụng bằng cách mock MessageService và đưa vào các Service tại thời điểm chạy hơn là lúc biên dịch

Tiếp theo chúng ta đã sẵn sàng viết các class Injector và các class Consumer

Injector class

Chúng tao tạo một interface MessageServiceInjector với phương thức khai báo trả về Consumer class

package com.example.java.dependencyinjection.injector;
import com.example.java.dependencyinjection.consumer.Consumer;

public interface MessageServiceInjector {
  public Consumer getConsumer();
}

Tiếp theo cho tất cả các Service, chúng ta sẽ tạo các class Injector như sau

package com.example.java.dependencyinjection.injector;

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

public class EmailServiceInjector implements MessageServiceInjector {

  @Override
  public Consumer getConsumer() {
    return new MyDIApplication(new EmailServiceImpl());
  }
}
package com.example.java.dependencyinjection.injector;

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

public class SMSServiceInjector implements MessageServiceInjector {

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

Hãy cùng xem các ứng dụng Client sử dụng như thế nào

package com.example.java.dependencyinjection.test;

import com.example.java.dependencyinjection.consumer.Consumer;
import com.example.java.dependencyinjection.injector.EmailServiceInjector;
import com.example.java.dependencyinjection.injector.MessageServiceInjector;
import com.example.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);
  }
}

Bạn có thể thấy rằng các class Application chỉ chịu trách nhiệm sử dụng Service. Các class Service được tạo trong các Injector. Ngoài ra nếu chúng ta phải mở rộng ứng dụng thêm nữa để cho phép gửi tin nhắn Facebook, chúng ta sẽ chỉ phải viết các class Service và class Injector

Vì thế phần thực thi Dependency Injection giải quyết được vấn đề hard-code của Dependency và giúp cho việc tạo ứng dụng thêm linh hoạt và dễ mở rộng. Tiếp theo hãy cùng xem giờ đây việc test ứng dụng đã trở nên dễ dàng như thế nào khi sử dụng mock cho các class Injector và Service.

JUnit Test Case

package com.example.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 injector sử dụng anonymous class
    injector = new MessageServiceInjector() {
			
      @Override
      public Consumer getConsumer() {
        //mock Messageservice
        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;
  }
}

Bạn có thể thấy rằng chúng ta đang sử dụng  anonymous classes để mock các class Injector và Service, nó giúp cho chúng ta dễ dàng test các phương thức của Application. Code bên trên đang sử dụng JUnit 4 cho class Test, vì thế cần đảm bảo rằng đường dẫn của code trên vẫn hoạt động tốt trong project của bạn.

Chúng ta đã sử dụng constructor để inject các Dependency vào trong class ứng dụng, một cách khác là chúng ta có thể sử dụng phương thức getter, hãy cùng xem code bên dưới đây

package com.example.java.dependencyinjection.consumer;

import com.example.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){
      // validate msg và tính toán logic 
      this.service.sendMessage(msg, rec);
    }
}
package com.example.java.dependencyinjection.injector;

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

public class EmailServiceInjector implements MessageServiceInjector {

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

Việc sử dụng constructor hoặc setter tuỳ thuộc vào yêu cầu bài toán để quyết định thiết kế. Ví dụ nếu ứng dụng có thể hoạt động hoàn toàn mà không cần class Service thì việc sử dụng Constructor cho DI là tốt hơn, ngoài ra thì sử dụng phương thức setter khi thực sự cần thiết

DI là cách để đạt được  Inversion of control (IoC) trong Application bằng cách chuyển các object ràng buộc nhau tại thời điểm biên dịch sang thời điểm chạy. Chúng ta có thể đạt được IoC bằng cách sử dụng Factory pattern, Template method pattern, Strategy pattern…

Ưu điểm

Sau đây là một vài ưu điểm của DI trong Java:

  • Tách biệt các quan hệ
  • Giảm boilerplate code trong class Application bởi vì tất cả công việc khởi tạo Dependency được xử lý bởi các Injector Component
  • Các Component được cấu hình giúp cho ứng dụng dễ mở rộng
  • Unit test dễ dàng hơn khi sử dụng mock các Object

Nhược điểm

DI có những nhược điểm như sau

  • Nếu lạm dụng nó dẫn đến việc khó bảo trì bởi vì ảnh hưởng của các thay đổi xảy ra tại thời điểm chạy
  • DI ẩn các class Service Dependency có thể dẫn đến lỗi khi chạy mà đến nhẽ có thể bắt khi biên dịch

 

You May Also Like

About the Author: Nguyen Dinh Thuc

Leave a Reply

Your email address will not be published.