IoC(Inversion of Control) は、制御の逆転を意味しますが、いったい何が逆転するのでしょうか。 これまでのフレームワークを使った開発では、アプリケーション側がフレームワークのAPIを呼び出すことが普通でした。あたりまえジャンと思うかもしれません。この場合、正しいAPIを正しいタイミングで呼び出すのは、 アプリケーション側の責任になります。
これが、IoCになるとフレームワークがアプリケーションのコードを呼び出すようになります。 アプリケーション側は、自分のやりたい最小限のコードを書いておけば、フレームワークが適切なタイミングで、アプリケーションのコードを呼び出してくれます。制御をフレームワークにまかせることで、 APIを間違って使ったりすることもなくなります。開発者の責任を減らすことができるため、アプリケーションの品質も向上します。
IoCコンテナは、IoCの考え方をコンポーネントのコンテナに応用したものです。EJBコンテナの代替としての側面もあります。 POJO(Plain Old Java Object:ぽじょ) に対し、EJBのようなトランザクション管理やリモート呼び出し機能を透過的に提供しようというものです。IoCコンテナを使うと次のようなことが実現できるようになります。
それでは、実際にIoCコンテナ上で動くコンポーネントを作成してみましょう。コンポーネントといっても、特別なインターフェースを実装する必要はなく、通常のJavaのオブジェクト(POJO)で大丈夫です。今回作成するコンポーネントの仕様は以下のようになります。
package examples.ioc.service;
public interface AutoNumber {
public int next(int numberKey) throws NumberKeyNotFoundRuntimeException;
}
また、DAOのコンポーネントとして以下のような仕様のコンポーネントを使います。
package examples.ioc.dao;
public interface AutoNumberDao {
public int inclement(int numberKey);
public int getCurrentNumber(int numberKey);
}
AutoNumberコンポーネントは、AutoNumberDaoコンポーネントの参照を必要とします。参照を取得するためには、コンストラクタ(Type3)やプロパティ(Type2)を使う方法がありますが、今回は、コンストラクタを使います。
package examples.ioc.service;
import examples.ioc.dao.AutoNumberDao;
public class AutoNumberImpl implements AutoNumber {
private AutoNumberDao autoNumberDao_;
public AutoNumberImpl(AutoNumberDao autoNumberDao) {
autoNumberDao_ = autoNumberDao;
}
public int next(int numberKey) throws NumberKeyNotFoundRuntimeException {
int updateCount = autoNumberDao_.inclement(numberKey);
if (updateCount == 1) {
return autoNumberDao_.getCurrentNumber(numberKey);
} else {
throw new NumberKeyNotFoundRuntimeException(numberKey);
}
}
}
コンストラクタの引数であるAutoNumberDaoは誰が設定してくれるのでしょうか?これまでは、アプリケーションのコードで設定するのが普通でした。IoCコンテナを使った場合はこのような依存関係は、IoCコンテナが解決してくれます。
それでは、AutoNumberImplをテストしてみましょう。テストするためには、AutoNumberDaoを実装したクラスが必要です。実際にデータベースには接続しなくても良いように、モックとなるDummyAutoNumberDaoを作成します。採番キーが1の場合は正常に処理し、それ以外は例外を発生させるような簡易実装になってます。
package test.examples.ioc.service;
import examples.ioc.dao.AutoNumberDao;
public class DummyAutoNumberDao implements AutoNumberDao {
private int number_;
public int inclement(int numberKey) {
if (numberKey == 1) {
number_++;
return 1;
} else {
return 0;
}
}
public int getCurrentNumber(int numberKey) {
if (numberKey == 1) {
return number_;
} else {
return 0;
}
}
}
SeasarのIoCコンテナを使うには、S2ContainerImplをnewして使います。
S2Container container = new S2ContainerImpl();
コンポーネントの登録は、S2Container#register(Class コンポーネントのクラス)を使います。
container.register(AutoNumberImpl.class); container.register(DummyAutoNumberDao.class);
コンポーネントを取得するには、S2Container#getComponent(Class コンポーネントのクラス)を使います。コンポーネントのクラスは、実装クラスも指定できますがインターフェースのクラスを指定したほうが良いでしょう。
AutoNumber autoNumber = (AutoNumber) container.getComponent(AutoNumber.class);
S2Containerで最低限知らなければいけないのはこれだけです。コンポーネントの依存関係は、型により自動的に解決されます。今回の例は、コンストラクタを使ってますが、プロパティを使う場合でも、同様に型により依存関係は自動的に解決されます。次のバージョンでは、XMLベースの設定もサポートします。
テストケースは以下のようになります。
package test.examples.ioc.service;
import org.seasar.framework.container.S2Container;
import org.seasar.framework.container.impl.S2ContainerImpl;
import examples.ioc.service.AutoNumber;
import examples.ioc.service.AutoNumberImpl;
import examples.ioc.service.NumberKeyNotFoundRuntimeException;
import junit.framework.TestCase;
public class AutoNumberImplTest extends TestCase {
private AutoNumber autoNumber_;
public AutoNumberImplTest(String arg0) {
super(arg0);
}
public static void main(String[] args) {
junit.textui.TestRunner.run(AutoNumberImplTest.class);
}
protected void setUp() throws Exception {
S2Container container = new S2ContainerImpl();
container.register(AutoNumberImpl.class);
container.register(DummyAutoNumberDao.class);
autoNumber_ = (AutoNumber) container.getComponent(AutoNumber.class);
}
public void testNext() {
assertEquals("1", 1, autoNumber_.next(1));
assertEquals("2", 2, autoNumber_.next(1));
}
public void testNextForNumberKeyNotFound() {
try {
autoNumber_.next(-1);
fail("1");
} catch (NumberKeyNotFoundRuntimeException ex) {
assertEquals("2", -1, ex.getNumberKey());
}
}
}
IoCコンテナ上で生息するコンポーネントは、コンポーネントの構成に必要な値をプロパティ(Type2)で設定するのか、コンストラクタ(Type3)で設定するかによって、タイプが分かれます。どちらが良いのかは好みの問題になってしまうのですが、私は、コンストラクタベース(Type3)が良いと考えています。よく言われるType3のメリット・デメリットは以下のようなものです。
私は、コンテナで管理する業務ロジックコンポーネントは、 ステートレスなサービスで他のコンポーネントとはできるだけ疎結合にすべきだと考えているので、コンストラクタの引数がそれほど増えることはないと思ってます。典型的なサービスは、AutoNumberImplのようにDAOへの参照だけを持ち、DAOはDataSourceへの参照だけを持っているような感じになると思います。
他のコンポーネントとの依存関係があまりなければ、テストに時に特定のプロパティだけを設定したいなんてことも問題にはなりません。
循環参照が生じる場合は、サービスの正規化に失敗しているのではないかと思います。そのような場合には、互いに参照し合ってるメソッドを共通サービスにくくりだすなどの対応を取った方がいいと考えます。Type2だと循環参照があっても実装できてしまいますが、Type3だとエラーになってしまうので、確実に気づくことができます。
コンポーネントの依存関係は、型によって自動的に解決されますが、 1つの型に複数のコンポーネントが登録されている場合や、 Stringやintなどの基本的な値を設定したい場合、 手動でコンストラクタを設定する必要があります。例えば、Integerのコンストラクタにintの0を渡したい場合次のようにします。
S2Container container = new S2ContainerImpl();
ComponentDef componentDef = new ComponentDefImpl(Integer.class, "foo");
ArgDef argDef = new ArgDefImpl(new Integer(0));
componentDef.addArgDef(argDef);
container.register(componentDef);
System.out.println(container.getComponent("foo"));
コンポーネントは、ComponentDefImplを使って定義します。コンストラクタの最初の引数には、コンポーネントのクラスを指定します。2番目の引数はコンポーネントの名前を指定します。名前をつけるとS2Container.getComponent(コンポーネント名)のように名前でコンポーネントが取得できます。名前をつけない場合は、 S2Container.getComponent(型)のように型でコンポーネントを取得します。S2Container.register(型)はS2Container.register(new ComponentDefImpl(型))と同じです。
引数は、ArgDefImplを使って定義します。コンストラクタの引数は、コンポーネントに渡したいオブジェクトを指定します。intのようなプリミティブ型を渡したい場合、ラッパークラスを使います。引数が複数ある場合には、ComponentDef.addArgDef()を複数回呼び出します。ArgDef.setChildComponentDef()を使い、再帰的にコンポーネントを組み立てることもできます。
プロパティも手動で設定することができます。例えば、Dateのプロパティtimeに0を設定したい場合次のようにします。
S2Container container = new S2ContainerImpl();
ComponentDef componentDef = new ComponentDefImpl(Date.class);
PropertyDef propertyDef = new PropertyDefImpl("time", new Long(0));
componentDef.addPropertyDef(propertyDef);
container.register(componentDef);
System.out.println(container.getComponent(Date.class));
プロパティは、PropertyDefImplを使って定義します。コンストラクタの最初の引数は、プロパティ名です。2番目の引数はコンポーネントに渡したいオブジェクトを指定します。longのようなプリミティブ型を渡したい場合、ラッパークラスを使います。プロパティが複数ある場合には、ComponentDef.addPropertyDef()を複数回呼び出します。PropertyDef.setChildComponentDef()を使い、再帰的にコンポーネントを組み立てることもできます。
コンストラクタの引数やプロパティに値を直接指定するのではなく、他のコンポーネントの参照を指定することもできます。例えば、DateのプロパティtimeにIntegerコンポーネントfooを設定したい場合次のようにします。
S2Container container = new S2ContainerImpl();
ComponentDef componentDef = new ComponentDefImpl(Integer.class, "foo");
ArgDef argDef = new ArgDefImpl(new Integer(0));
componentDef.addArgDef(argDef);
container.register(componentDef);
ComponentDef componentDef2 = new ComponentDefImpl(Date.class);
PropertyDef propertyDef = new PropertyDefImpl("time");
propertyDef.setExpression("foo");
componentDef2.addPropertyDef(propertyDef);
container.register(componentDef2);
System.out.println(container.getComponent(Date.class));
他のコンポーネントを参照するには、ComponentDef.setExpression(コンポーネント名)を使います。同様に、ArgDef.setExpression(コンポーネント名)も使えます。
コンポーネントによっては、プロパティやコンストラクタだけでなく、メソッドを呼び出して値を設定したい場合もあります。例えば、HashMapのputメソッドを呼び出したい場合次のようにします。
S2Container container = new S2ContainerImpl();
ComponentDef componentDef = new ComponentDefImpl(HashMap.class);
MethodDef methodDef = new MethodDefImpl("put");
ArgDef arg1Def = new ArgDefImpl("aaa");
ArgDef arg2Def = new ArgDefImpl("111");
methodDef.addArgDef(arg1Def);
methodDef.addArgDef(arg2Def);
componentDef.addMethodDef(methodDef);
container.register(componentDef);
System.out.println(container.getComponent(Map.class));
メソッドは、MethodDefImplを使って定義します。コンストラクタの引数は、メソッド名です。メソッドの引数はArgDefImplで定義し、MethodDef.addArgDef()で追加します。
コンポーネントにAOPを適用することもできます。例えば、ArrayListにTraceAdviceを適用したい場合次のようにします。
S2Container container = new S2ContainerImpl(); ComponentDef componentDef = new ComponentDefImpl(ArrayList.class); AspectDef aspectDef = new AspectDefImpl(new TraceAdvice()); componentDef.addAspectDef(aspectDef); container.register(componentDef); List list = (List) container.getComponent(List.class); list.size();
Aspectは、AspectDefImplを使って定義します。コンストラクタの最初の引数は、AroundAdviceのインスタンスです。2番目の引数でPointcutを指定することもできます。Pointcutを指定しなかった場合は、コンポーネントが実装しているインターフェースのすべてのメソッドが対象になります。