前回:[Java]Spring Framework 2.5入門 「依存性の注入」
前回までで、Spring Framework2.5のDIコンテナとしての基本的な機能は使えるようになってきました。
といっても、beanタグとpropertyタグとconstructor-argタグを使うだけですが。今回は、DIコンテナのメリットを書いてみようと思います。
一般的にDIコンテナを使ったプログラムでは、インターフェースを定義して、それを実装したクラスをDIコンテナに登録して使います。これまでのエントリでは、インターフェースは使わずに具象クラスのみ使っていました。
ということで、前回やったMyServiceとMyLogicをインターフェースを使うように変えてみます。
下記の4つを定義します。
- springbean.beans.MyLogicインターフェース
- springbean.beans.MyLogicImplクラス
- springbean.beans.MyServiceインターフェース
- springbean.beans.MyServiceImplクラス
実装は特に悩むことはなく、単純にインターフェースと実装クラスのペアをつくっていくだけです。
MyLogicインターフェース
package springbean.beans;
public interface MyLogic {
int add(int lhs, int rhs);
}
MyLogicImplクラス
package springbean.beans;
public class MyLogicImpl implements MyLogic {
public int add(int lhs, int rhs) {
return lhs + rhs;
}
}
MyServiceインターフェース
package springbean.beans;
public interface MyService {
int execute();
}
MyServiceImplクラス
package springbean.beans;
public class MyServiceImpl implements MyService {
// インターフェースを受け取るようにする
private MyLogic logic;
private int lhs;
private int rhs;
public int execute() {
return logic.add(lhs, rhs);
}
public void setLhs(int lhs) {
this.lhs = lhs;
}
public void setLogic(MyLogic logic) {
this.logic = logic;
}
public void setRhs(int rhs) {
this.rhs = rhs;
}
}
ここで大事なのが、MyServiceImplクラスが依存するものがMyLogicインターフェースになるという部分です。MyLogicインターフェースを実装するクラスなら何でもMyServiceImplに設定することが出来るようになります。
当然application.xmlも変更が入ります。MyLogicImplとMyServiceImplを定義するように変更します。
application.xml(beansタグの内側だけ抜粋)
<bean id="myLogic" class="springbean.beans.MyLogicImpl" />
<bean id="myService" class="springbean.beans.MyServiceImpl">
<property name="logic" ref="myLogic" />
<property name="lhs" value="100" />
<property name="rhs" value="200" />
</bean>
Mainは特に変更はありませんが一応mainメソッド内部だけのせておきます。
ApplicationContext ctx = new ClassPathXmlApplicationContext("application.xml");
// インターフェースに対してプログラムを書く
MyService service = (MyService) ctx.getBean("myService");
System.out.println("service#execute = " + service.execute()); // 300と表示される
これで、一般的なDIコンテナを使ったプログラムっぽい形になりました。正直、こんなめんどくさいことを何故するんだろう?と最初思いましたが、こうすることで”ユニットテストが非常にやりやすい”形になっています。
MyServiceImplクラス内には、外部のクラスに直接依存する部分は排除される形になっています。MyServiceImplクラスを動かすための要件は、MyLogicインターフェースを実装したクラスがあるということだけになります。
つまり、テストをしたければ、MyLogicインターフェースを実装したMyLogicMockみたいなクラスを作って、それを使ってテストが出来るようになります。
ということでやってみましょう。Spring Frameworkには単体テストを支援する色々な機能が組み込まれていますが、ここでは、その機能は使わずに書いてみます。
MyServiceImplが単体として満たさなければいけない要件は、以下の2つになると思います。
- logicのaddメソッドにlhsフィールド, rhsフィールドの値をちゃんと渡す。
- logicのaddメソッドの結果をちゃんと返す。
ということで、早速MyLogicインターフェースを実装したMockクラスを定義します。
package springbean.beans;
import static org.junit.Assert.*;
public class MyLogicMock implements MyLogic {
public int add(int lhs, int rhs) {
// 要件1をちゃんと満たしているか確認
assertEquals(100, lhs);
assertEquals(200, rhs);
return 1000; // 固定値を返す
}
}
このMockクラスは、引数が100と200がわたってきているか確認した後に、固定値で1000を返します。このMockクラスを使うように定義したspringbean.beans.MyServiceTest-context.xmlファイルを作成します。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<!-- ロジックはMockを使う -->
<bean id="logic" class="springbean.beans.MyLogicMock" />
<bean id="service" class="springbean.beans.MyServiceImpl">
<property name="logic" ref="logic" />
<property name="lhs" value="100" />
<property name="rhs" value="200" />
</bean>
</beans>
テストクラスのセットアップで、この定義ファイルを読み込んでApplicationContextを作ってserviceを取得します。
そして、取得したserviceに対してテストを実施します。
package springbean.beans;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import static org.junit.Assert.*;
public class MyServiceTest {
private MyService service;
@Before
public void setUp() {
// テスト用の定義ファイル読み込み
ApplicationContext ctx = new ClassPathXmlApplicationContext(
"springbean/beans/MyServiceTest-context.xml");
service = (MyService) ctx.getBean("service");
}
@After
public void tearDown() {
service = null;
}
@Test
public void execute() throws Exception {
// 要件2を満たしているか確認
assertEquals(1000, service.execute());
}
}
といった感じにクラス間が疎結合になり、単体テストがとてもやりやすくなるのがDIコンテナのメリットの1つでもあります。
今回の例では、単純すぎてあんまりメリットは感じないですが、たとえばMyLogicがDBにアクセスするような処理だったら、Mockに簡単に差し替えることでDBに依存しないでMyServiceのテストが出来るということは大きなメリットになると思います。
ということで今回はおしまい。