Javaの依存性注入 – DIデザインパターンの例とチュートリアル

Javaの依存性注入デザインパターンを使用すると、ハードコーディングされた依存関係を取り除いて、アプリケーションを疎結合化、拡張可能性と保守性の高いものにすることができます。Javaで依存性注入を実装することで、依存関係の解決をコンパイル時から実行時に移すことができます。

ジャヴァの依存性注入

Javaの依存性注入は理論的には理解しづらいようですので、簡単な例を使って依存性注入パターンを使ってアプリケーション内での疎結合性と拡張性を実現する方法を見てみましょう。例えば、私たちがEmailServiceを使用してメールを送るアプリケーションがあるとします。通常、以下のように実装することになります。

package com.scdev.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クラスは、受信者のメールアドレスにメールメッセージを送信するためのロジックを保持しています。以下のように、アプリケーションコードは次のようになります。

package com.scdev.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);
	}
}

下記のように、MyApplicationクラスを使用して電子メールメッセージを送信するためのクライアントコードがあるでしょう。

package com.scdev.java.legacy;

public class MyLegacyTest {

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

}

最初に見る限り、上記の実装には問題がないように思える。しかし、上記のコードの論理には特定の制限があります。

  • MyApplication class is responsible to initialize the email service and then use it. This leads to hard-coded dependency. If we want to switch to some other advanced email service in the future, it will require code changes in MyApplication class. This makes our application hard to extend and if email service is used in multiple classes then that would be even harder.
  • If we want to extend our application to provide an additional messaging feature, such as SMS or Facebook message then we would need to write another application for that. This will involve code changes in application classes and in client classes too.
  • Testing the application will be very difficult since our application is directly creating the email service instance. There is no way we can mock these objects in our test classes.

MyApplicationクラスから、メールサービスのインスタンス作成を削除するために、メールサービスを引数とするコンストラクタを持つことができると主張することができる。

package com.scdev.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);
	}
}

しかし、この場合は、クライアントアプリケーションやテストクラスにメールサービスの初期化を要求するというのは良いデザインの決定ではありません。それでは、上記の実装のすべての問題を解決するために、Javaの依存性注入パターンをどのように適用できるか見てみましょう。Javaにおける依存性注入には、少なくとも以下のものが必要です。

    1. サービスのコンポーネントは、ベースクラスまたはインターフェースで設計するべきです。サービスの契約を定義するインターフェースや抽象クラスを好む方が良いでしょう。

 

    1. コンシューマクラスは、サービスインターフェースに基づいて記述すべきです。

 

    サービスを初期化し、それからコンシューマクラスを実行するインジェクタクラスが必要です。

Javaの依存性注入 – サービスのコンポーネント

私たちの場合、サービス実装のための契約を宣言するMessageServiceを持つことができます。

package com.scdev.java.dependencyinjection.service;

public interface MessageService {

	void sendMessage(String msg, String rec);
}

では、上記のインタフェースを実装したメールとSMSのサービスがあるとしましょう。

package com.scdev.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.scdev.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);
	}

}

私たちの依存性注入Javaサービスは準備が整っており、今は消費者クラスを書くことができます。

Java依存性注入 – サービスの利用者

私たちは、消費者クラスに基本インターフェースを持つ必要はありませんが、私は消費者クラスの契約を宣言するためにConsumerインターフェースを持つことにします。

package com.scdev.java.dependencyinjection.consumer;

public interface Consumer {

	void processMessages(String msg, String rec);
}

私の消費者クラスの実装は以下のようです。

package com.scdev.java.dependencyinjection.consumer;

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

}

以下のように日本語での言い換えを提案します:

私たちのアプリケーションクラスは、単にサービスを使用していることに注意してください。サービスの初期化は行わず、「関心の分離」を改善しています。また、サービスインターフェースの使用により、メッセージサービスをモック化してアプリケーションを簡単にテストし、サービスを実行時にバインドすることができます。これで、サービスとコンシューマクラスを初期化するためのJavaの依存性インジェクタクラスを書く準備ができました。

Java依存性注入 – インジェクタークラス

インターフェースMessageServiceInjectorを持ち、Consumerクラスを返すメソッドの宣言があります。

(Let’s) インターフェースMessageServiceInjectorで、Consumerクラスを返すメソッドの宣言をしましょう。

package com.scdev.java.dependencyinjection.injector;

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

public interface MessageServiceInjector {

	public Consumer getConsumer();
}

今後、すべてのサービスについて、以下のようなインジェクタークラスを作成する必要があります。

package com.scdev.java.dependencyinjection.injector;

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

public class EmailServiceInjector implements MessageServiceInjector {

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

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

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

public class SMSServiceInjector implements MessageServiceInjector {

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

}

では、簡単なプログラムを使って、クライアントアプリケーションがどのように当アプリケーションを使用するかを見てみましょう。

package com.scdev.java.dependencyinjection.test;

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

public class MyMessageDITest {

	public static void main(String[] args) {
		String msg = "Hi Pankaj";
		String email = "scdev@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);
	}

}

私たちのアプリケーションクラスは、サービスの使用にだけ責任を持っていることがわかります。サービスクラスはインジェクターで作成されます。また、Facebookメッセージングを許可するためにアプリケーションをさらに拡張する必要がある場合、サービスクラスとインジェクタークラスのみを作成する必要があります。したがって、依存性注入の実装により、ハードコードされた依存関係の問題が解決され、アプリケーションが柔軟で拡張しやすくなりました。さて、インジェクターとサービスクラスをモック化してアプリケーションクラスを簡単にテストできるか見てみましょう。

モックインジェクターとサービスを使用したJUnitテストケースにおけるJavaの依存性注入

package com.scdev.java.dependencyinjection.test;

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

import com.scdev.java.dependencyinjection.consumer.Consumer;
import com.scdev.java.dependencyinjection.consumer.MyDIApplication;
import com.scdev.java.dependencyinjection.injector.MessageServiceInjector;
import com.scdev.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", "scdev@abc.com");
	}
	
	@After
	public void tear(){
		injector = null;
	}

}

上のテストクラスを実行する場合、私はインジェクターとサービスクラスをモックするために匿名クラスを使用していることがわかります。私はアプリケーションのメソッドを簡単にテストすることができます。上記のテストクラスではJUnit 4を使用しているため、プロジェクトのビルドパスに組み込まれていることを確認してください。アプリケーションクラスでは依存関係をインジェクトするためにコンストラクターを使用していますが、別の方法としてセッターメソッドで依存関係をインジェクトすることもできます。セッターメソッドを使用した依存性のインジェクションについては、以下のようにアプリケーションクラスを実装します。

package com.scdev.java.dependencyinjection.consumer;

import com.scdev.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.scdev.java.dependencyinjection.injector;

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

public class EmailServiceInjector implements MessageServiceInjector {

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

}

セッターによる依存性注入の最高の例の一つは、Struts2 Servlet API Aware インターフェースです。コンストラクタベースの依存性注入を使用するか、セッターベースの依存性注入を使用するかは、設計上の決定であり、要件に依存します。たとえば、サービスクラスなしではアプリケーションがまったく動作しない場合は、コンストラクタベースのDIを選択するか、それ以外の場合は本当に必要な時にのみ使用するため、セッターメソッドベースのDIを選択します。Javaにおける依存性注入は、コンパイル時から実行時へのオブジェクトのバインディングを移動することによって、アプリケーションに逆制御(IoC)を実現するための方法です。IoCは、ファクトリーパターン、テンプレートメソッドデザインパターン、ストラテジーパターン、およびサービスロケーターパターンを通じて実現することができます。Spring依存性注入、Google Guice、およびJava EE CDIフレームワークは、Java Reflection APIとJavaアノテーションの使用を通じて依存性注入のプロセスを容易にします。フィールド、コンストラクタ、またはセッターメソッドにアノテーションを付け、それらを構成XMLファイルまたはクラスに設定する必要があります。

Javaの依存性注入の利点

JavaでDependency Injectionを使用することのいくつかの利点は次のとおりです。

  • Separation of Concerns
  • Boilerplate Code reduction in application classes because all work to initialize dependencies is handled by the injector component
  • Configurable components makes application easily extendable
  • Unit testing is easy with mock objects

Javaの依存性注入のデメリット

Javaの依存性注入にはいくつかのデメリットもあります。

  • If overused, it can lead to maintenance issues because the effect of changes are known at runtime.
  • Dependency injection in java hides the service class dependencies that can lead to runtime errors that would have been caught at compile time.

依存性注入プロジェクトをダウンロードしてください。

Javaにおける依存性注入パターンについては以上です。サービスを制御できる場合には、知って使用するのが良いです。

コメントを残す 0

Your email address will not be published. Required fields are marked *