Javaの依存性注入デザインパターンを使用すると、ハードコーディングされた依存関係を除去し、アプリケーションを疎結合化、拡張可能、保守可能にすることができます。私たちはJavaで依存性注入を実装することで、依存性の解決をコンパイル時からランタイムに移動することができます。
Javaの依存性注入
Javaの依存性注入は理論的には理解しにくいように思えますので、簡単な例を挙げて、どのように依存性注入パターンを使用してアプリケーションの疎結合化と拡張性を実現するかを見てみましょう。例えば、EmailService
を使用して電子メールを送信するアプリケーションがあるとします。通常、以下のように実装します。
package com.journaldev.java.legacy;
public class EmailService {
public void sendEmail(String message, String receiver){
//電子メールを送信するためのロジック
System.out.println("Email sent to "+receiver+ " with Message="+message);
}
}
EmailService
クラスは、受信者のメールアドレスにメールメッセージを送信するためのロジックを保持しています。アプリケーションのコードは以下のようになります。
package com.journaldev.java.legacy;
public class MyApplication {
private EmailService email = new EmailService();
public void processMessages(String msg, String rec){
//メッセージの検証、操作ロジックなどを行う
this.email.sendEmail(msg, rec);
}
}
MyApplication
クラスを使用して電子メールメッセージを送信するクライアントコードは以下のようになります。
package com.journaldev.java.legacy;
public class MyLegacyTest {
public static void main(String[] args) {
MyApplication app = new MyApplication();
app.processMessages("Hi Pankaj", "[email protected]");
}
}
最初の見た目では、上記の実装には問題がないように思えます。しかし、上記のコードロジックにはいくつかの制限があります。
MyApplication
クラスは、メールサービスを初期化し、それを使用する責任があります。これにより、ハードコードされた依存関係が生じます。将来的に他の高度なメールサービスに切り替えたい場合、MyApplication
クラスのコードを変更する必要があります。これにより、アプリケーションの拡張が困難になり、メールサービスが複数のクラスで使用されている場合はさらに困難になります。- アプリケーションを拡張して、SMSやFacebookメッセージなどの追加のメッセージング機能を提供する場合、別のアプリケーションを作成する必要があります。これには、アプリケーションクラスとクライアントクラスの両方でコードの変更が必要です。
- アプリケーションのテストは非常に困難になります。なぜなら、アプリケーションが直接メールサービスのインスタンスを作成しているからです。テストクラスでこれらのオブジェクトをモックする方法はありません。
誰かが主張するかもしれませんが、MyApplication
クラスからメールサービスのインスタンス作成を削除することができます。そのためには、メールサービスを引数として必要とするコンストラクタを持つことが必要です。
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){
//メッセージのバリデーション、操作ロジックなどを行う
this.email.sendEmail(msg, rec);
}
}
しかし、この場合、クライアントアプリケーションやテストクラスにメールサービスの初期化を依頼しています。これは良い設計上の決定ではありません。では、上記の実装の問題を解決するために、Javaの依存性注入パターンをどのように適用できるか見てみましょう。Javaの依存性注入には、少なくとも以下のものが必要です:
- サービスコンポーネントは、基本クラスまたはインターフェースで設計する必要があります。サービスの契約を定義するために、インターフェースや抽象クラスを優先することが望ましいです。
- コンシューマクラスは、サービスインターフェースの形式で記述する必要があります。
- サービスを初期化し、それからコンシューマクラスを初期化するインジェクタクラス。
Java依存性注入 – サービスコンポーネント
この場合、サービス実装のための契約を宣言するMessageService
を持つことができます。
package com.journaldev.java.dependencyinjection.service;
public interface MessageService {
void sendMessage(String msg, String rec);
}
今、上記のインターフェースを実装するEmailとSMSサービスがあるとしましょう。
package com.journaldev.java.dependencyinjection.service;
public class EmailServiceImpl implements MessageService {
@Override
public void sendMessage(String msg, String rec) {
//メールを送信するロジック
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) {
//SMSを送信するロジック
System.out.println("SMS sent to "+rec+ " with Message="+msg);
}
}
依存性注入のJavaサービスが準備できましたので、コンシューマクラスを書くことができます。
Java依存性注入 – サービスコンシューマ
コンシューマクラスに基本インターフェースを持つ必要はありませんが、コンシューマクラスの契約を宣言するConsumer
インターフェースを持つことにします。
package com.journaldev.java.dependencyinjection.consumer;
public interface Consumer {
void processMessages(String msg, String rec);
}
コンシューマクラスの実装は以下のようになります。
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){
//メッセージの検証や操作ロジックなどを行う
this.service.sendMessage(msg, rec);
}
}
私たちのアプリケーションクラスは、サービスを使用するだけであり、サービスの初期化は行っていません。これにより、関心の分離が向上しています。また、サービスインターフェースの使用により、MessageServiceをモック化してアプリケーションを簡単にテストし、サービスをコンパイル時ではなくランタイムでバインドすることができます。これで、サービスを初期化し、コンシューマクラスを作成するためのJavaの依存性インジェクションクラスを書く準備が整いました。
Javaの依存性インジェクション – インジェクタクラス
まず、MessageServiceInjector
というインターフェースを作成し、Consumer
クラスを返すメソッド宣言を行います。
package com.journaldev.java.dependencyinjection.injector;
import com.journaldev.java.dependencyinjection.consumer.Consumer;
public interface MessageServiceInjector {
public Consumer getConsumer();
}
次に、以下のように各サービスごとにインジェクタクラスを作成する必要があります。
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());
}
}
さて、クライアントアプリケーションが簡単なプログラムでアプリケーションを使用する方法を見てみましょう。
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;
//メールを送信
injector = new EmailServiceInjector();
app = injector.getConsumer();
app.processMessages(msg, email);
//SMSを送信
injector = new SMSServiceInjector();
app = injector.getConsumer();
app.processMessages(msg, phone);
}
}
わかるように、アプリケーションクラスはサービスの使用のみに責任を持っています。サービスクラスはインジェクタで作成されます。また、アプリケーションをさらに拡張してFacebookメッセージングを許可する場合、サービスクラスとインジェクタクラスのみを作成する必要があります。したがって、依存性インジェクションの実装により、ハードコーディングされた依存関係の問題が解決され、アプリケーションが柔軟かつ簡単に拡張できるようになりました。さて、インジェクタとサービスクラスをモック化してアプリケーションクラスを簡単にテストできるかどうかを見てみましょう。
Javaの依存性注入 – モックインジェクタとサービスを使用した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(){
//匿名クラスを使用してインジェクタをモック化する
injector = new MessageServiceInjector() {
@Override
public Consumer getConsumer() {
//メッセージサービスをモック化する
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;
}
}
上記のコードでは、匿名クラスを使用してインジェクタとサービスクラスをモック化し、簡単にアプリケーションのメソッドをテストしています。上記のテストクラスではJUnit 4を使用しているため、プロジェクトのビルドパスに含まれていることを確認してください。アプリケーションクラスでは、コンストラクタを使用して依存関係を注入していますが、別の方法としてセッターメソッドを使用してアプリケーションクラスに依存関係を注入することもできます。セッターメソッドによる依存性注入の場合、アプリケーションクラスは以下のように実装されます。
package com.journaldev.java.dependencyinjection.consumer;
import com.journaldev.java.dependencyinjection.service.MessageService;
public class MyDIApplication implements Consumer{
private MessageService service;
public MyDIApplication(){}
//セッターメソッドによる依存性注入
public void setService(MessageService service) {
this.service = service;
}
@Override
public void processMessages(String msg, String rec){
//メッセージのバリデーション、操作ロジックなどを行う
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;
}
}
Struts2 Servlet API Awareインターフェースは、セッターベースの依存性注入の最良の例の1つです。コンストラクタベースの依存性注入を使用するか、セッターベースの依存性注入を使用するかは、設計上の決定であり、要件に依存します。たとえば、サービスクラスなしではアプリケーションがまったく機能しない場合は、コンストラクタベースのDIを選択し、それ以外の場合は本当に必要な場合にのみ使用するためにセッターメソッドベースのDIを選択します。Javaでの依存性注入は、オブジェクトのバインディングをコンパイル時からランタイムに移動することで、制御の反転(IoC)を実現する方法です。IoCはファクトリーパターン、テンプレートメソッドデザインパターン、ストラテジーパターン、およびサービスロケーターパターンを介して実現することもできます。Spring Dependency Injection、Google Guice、およびJava EE CDIフレームワークは、Java Reflection APIとJavaアノテーションの使用によって依存性注入のプロセスを容易にします。必要なのは、フィールド、コンストラクタ、またはセッターメソッドに注釈を付け、それらを設定ファイルやクラスで構成するだけです。
Java依存性注入の利点
Javaで依存性注入を使用することの利点の一部は以下の通りです:
- 関心の分離
- アプリケーションクラスのボイラープレートコードの削減、すべての依存関係の初期化作業はインジェクタコンポーネントによって処理されます
- 設定可能なコンポーネントにより、アプリケーションを容易に拡張できます
- モックオブジェクトを使用した単体テストが容易です
Java依存性注入の欠点
Javaの依存性注入にはいくつかの欠点もあります:
- 過度に使用すると、変更の影響が実行時にのみわかるため、メンテナンスの問題を引き起こす可能性があります。
- Javaの依存性注入は、コンパイル時に検出されるはずだったランタイムエラーを引き起こす可能性のあるサービスクラスの依存関係を隠します。
以上がJavaの依存性注入パターンについての情報です。サービスを制御できる状況で知っておき、使用することは良いことです。