Giới thiệu về Design Pattern Dependency Injection

dependency-injection.jpg

Dependency Injection là một design pattern tuyệt vời cho phép chúng ta loại bỏ sự phụ thuộc cứng nhắc giữa các phần tử và làm cho ứng dụng trở nên linh hoạt mềm dẻo hơn, dễ mở rộng, dễ bảo trì.

Dependency Injection là một khái niệm khá khó nắm bắt nếu chỉ tìm hiểu trên phương diện lý thuyết. Do vậy, nhằm giúp các bạn dễ hiểu hơn về design pattern này, mình xin được lấy một ví dụ cơ bản được viết bằng ngôn ngữ Java.

Chẳng hạn chúng ta có một ứng dụng sử dụng EmailService để thực hiện việc gửi mail, thì thông thường mã nguồn sẽ như sau:

package com.di.java.legacy;

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

Class EmailService có phương thức sendEmail bao gồm 2 tham số truyền vào, yêu cầu người gửi phải có nội dung thư và địa chỉ email của người nhận. Tiếp theo, chúng ta có code ứng dụng như sau:

package com.di.java.legacy;

public class MyApplication {
    private EmailService email = new EmailService();

    public void processMessages(String msg, String receiver){
        // Thực hiện validate nội dung thư
        this.email.sendEmail(msg,receiver);
    }
}

Cuối cùng, chúng ta sẽ viết thêm code client sử dụng class MyApplication để gửi mail:

package com.di.java.legacy;

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

Như các bạn thấy, về cơ bản việc implement như trên không phải là sai, tuy nhiên nếu làm việc với một dự án lớn thì sẽ vô cùng hạn chế đối với team phát triển.

  • MyApplication phụ trách việc khởi tạo email service và sử dụng nó trong hàm processMessages. Như vậy sự phụ thuộc giữa MyApplication và email service đã bị fix cứng, trong tương lai nếu chúng ta muốn thêm vào một số chức năng khác cho email service thì chúng ta phải thay đổi code của class MyApplication. Trong trường hợp có nhiều class không chỉ riêng MyApplication sử dụng email service, thì việc mở rộng là vô cùng khó khăn.
  • Thứ hai, trong trường hợp chúng ta muốn mở rộng thêm một vài tính năng hữu ích khác như gửi tin nhắn SMS hay Facebook thì bắt buộc chúng ta lại phải viết thêm một ứng dụng khác, điều này sẽ dẫn đến việc phải thay đổi code khá nhiều trong nhóm những class application và client.
  • Test ứng dụng sẽ gặp nhiều khó khăn bởi ứng dụng của chúng ta lúc này khởi tạo trực tiếp luôn đối tượng email service, việc lấy những object này cho vào các class test là điều không thể

Một ý kiến cho rằng chúng ta có thể loại bỏ việc khởi tạo đối tượng email service ở trong class MyApplication bằng cách khai báo ra một constructor với tham số là email service như sau:

package com.di.java.legacy;

public class MyApplication {
    private EmailService email = null;

    public MyApplication(EmailService svc){
        this.email = svc;
    }
    public void processMessages(String msg, String receiver){
        // Thực hiện validate nội dung thư
        this.email.sendEmail(msg,receiver);
    }
}

Tuy nhiên, rõ ràng đây không phải là một cách thiết kế code tốt, bởi nếu làm theo cách này, chúng ta đang yêu cầu những ứng dụng client và những class test thực hiện việc khởi tạo email service.

Bây giờ, chúng ta hãy cùng xem Dependency Injection có thể làm gì để giải quyết toàn bộ những vấn đề gặp phải từ cách thiết kế code trên như sau. Về cơ bản, Dependency Injection yêu cầu phải thỏa mãn những tiêu chí sau:

  • Các thành phần của service nên được viết cùng với một lớp cơ sở (base class) hoặc một interface.
  • Những class sử dụng service nên sử dụng những interface mà những service cần dùng implement
  • Phải có nhóm những class injector phụ trách việc khởi tạo service và sau đó là những class sử dụng service ấy.

Sau khi xem xét xong những tiêu chí trên, chúng ta tạo một interface có tên MessageService để service implement:

package com.di.java.dependencyinjection.service;

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

Tiếp theo, chúng ta có Email và SMS service implement interface này:

package com.di.java.dependencyinjection.service;

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

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

Đối với những class sử dụng service thì không nhất thiết phải có một interface cơ sở. Trong ví dụ này, mình sẽ sử dụng một interface cơ sở có tên Consumer:

package com.di.java.dependencyinjection.consumer;

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

Lớp Consumer implement interface này sẽ như sau:

package com.di.java.dependencyinjection.consumer;

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

public class MyDIApplication implements Consumer {
    private MessageService service;

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

    public void processMessages(String msg, String receiver){
        // Thực hiện validate msg
        this.service.sendMessage(msg,receiver);
    }
}

Cần lưu ý rằng với cách implement như trên, ứng dụng của chúng ta chỉ sử dụng chứ không khởi tạo bất kỳ một service nào. Như vậy sẽ giúp cho lập trình viên dễ test hơn bằng việc mocking object thuộc class MessageService và bind những service đó vào runtime.

Tiếp theo, chúng ta sẽ viết các class injector dùng để khởi tạo service và consumer. Trước tiên chúng ta có interface MessageServiceInjector với method trả về Consumer

package com.di.java.dependencyinjection.injector;

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

public interface MessageServiceInjector {
    public Consumer getConsumer();
}

Như vậy, đối với mỗi một service, chúng ta sẽ có một class injector như sau:

package com.di.java.dependencyinjection.injector;

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

public class EmailServiceInjector implements MessageServiceInjector {

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

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

public class SMSServiceInjector implements MessageServiceInjector {

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

Chúng ta sẽ có ứng dụng client như sau:

ackage com.di.java.dependencyinjection.test;

import com.di.java.dependencyinjection.consumer.Consumer;
import com.di.java.dependencyinjection.injector.EmailServiceInjector;
import com.di.java.dependencyinjection.injector.MessageServiceInjector;
import com.di.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ư các bạn có thể thấy, các class application của chúng ta chỉ sử dụng service. Các object của các class service được khởi tạo ở injector. Ngoài ra, nếu trong tương lai ứng dụng của chúng ta có hỗ trợ thêm gửi tin nhắn qua facebook nữa, thì chỉ cần viết thêm các class service và injector.

Một cách implement của dependency injection nữa ngoài cách sử dụng constructor đó là sử dụng hàm setter trong các class application.

package com.di.java.dependencyinjection.consumer;

import com.di.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;
	}

	public void processMessages(String msg, String receiver){
		// Thực hiện validate msg
		this.service.sendMessage(msg, receiver);
	}
}
package com.di.java.dependencyinjection.injector;

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

public class EmailServiceInjector implements MessageServiceInjector {

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

Một trong những ví dụ tiêu biểu nhất của việc dùng dependency injection là những interface thuộc Struts 2 Servlet API Aware.

Tổng kết lại, việc sử dụng dependency injection có những ưu và nhược điểm sau:

Ưu điểm

  • Tách mối quan tâm (Separation of Concerns)
  • Cắt giảm những đoạn code dùng chung trong các class application (chẳng hạn khởi tạo) vì toàn bộ công việc khởi tạo dependencies đã do injector lo
  • Dễ mở rộng
  • Unit test sẽ dễ dàng hơn với mock object

Nhược điểm

  • Nếu quá lạm dụng sẽ dẫn đến các vấn đề liên quan đến việc bảo trì bởi những thay đổi sẽ chỉ xuất hiện ở runtime

Tham khảo: journaldev