誰か彼かがDIコンテナについて過去に書いているかもしれませんが、再度まとめです。
DIコンテナのDIとはDependency Injectionです。直訳すると依存性の注入になります。そしてコンテナとはオブジェクトを複数管理する仕組みです。DIコンテナは、管理しているオブジェクト間の依存性を、ある程度自動的に解決する仕組みをもったコンテナとなります。
DIコンテナには2つの大きな目的と、1つの大きな拡張機能があります。それでは1つずつ見てみましょう。
依存性の解決
DIコンテナは再帰的に管理しているクラスの依存性を解決します。依存性とは大きく分けて2種類あります。それはコンストラクタの引数とプロパティです。例えば以下のようなクラス郡が存在するとします。
public class Car{
private Engine engine;
private Tire tire;
public Car(Engine engine, Tire tire){
this.engine = engine;
this.tire = tire;
}
}
public class Engine{
}
public class Tire{
private Wheel wheel;
public Tire(Wheel wheel){
this.wheel = wheel;
}
}
public class Wheel{
}
正しいCarのインスタンスが欲しい場合、以下のようなコードによる手続きが必要になります。
public class CarClient{
public static void main(String[] args){
Wheel wheel = new Wheel();
Tire tire = new Tire(wheel);
Engine engine = new Engine();
Car car = new Car(engine, wheel);
}
}
普通のコードで書けば手続きが複雑になり、手続きを間違えればバグの原因となります。もし適当なDIコンテナがあるとして、利用した場合以下のようなコードになります。
public class CarClient2{
public static void main(String[] args){
Container container = ContainerFactory.getContainer();
Car car = container.getInstance(Car.class);
}
}
サンプルのパターンで解説すると、クライアントはDIコンテナにCarのインスタンスを要求します。DIコンテナはCarのコンストラクタが依存しているEngineのインスタンスとTireのインスタンスをDIコンテナ自身に要求します。Engineは他の型に依存していませんが、TireはさらにWheelに依存します。ここでWheelのインスタンスを再度DIコンテナ自身に要求します。
この様にDIコンテナは自身の設定を元に、プロパティやコンストラクタが依存する型を再帰的に解決します。
疎結合のサポート
DIコンテナは疎結合をサポートする目的でも利用されます。疎結合とはクラス間の結合度が薄い状態です。Javaにおける結合度は、コンクリートクラスに対する依存度で計られます。
上記コードではCarはEngineというコンクリートクラスとTireというコンクリートクラスに依存します。CarとEngineとTireは同じ人が作れば問題はありません。しかしそれぞれベンダーが違う場合、綿密に打ち合わせしなければなりませんし、EngineとTireが存在しなければ、そもそもCarを実装する事はできません。そこで依存度を疎結合にするためにインターフェイスが登場します。
ではサンプルコードを見てみましょう。
まずはインターフェイス郡です。
public interface Tire{
}
public interface Engine{
}
そして実装です。
public class StudlessTire implements Tire{
private Wheel wheel;
public StudlessTire(Wheel wheel){
this.wheel = wheel;
}
}
public class SpikeTire implements Tire{
private Wheel wheel;
public SpikeTire (Wheel wheel){
this.wheel = wheel;
}
}
public class DieselEngine implements Engine{
}
public class GasolineEngine implements Engine{
}
上記のようなコードが存在するとき、DIコンテナはCarに必要なEngineとTireのインスタンスを一意に特定する事はできません。そのために設定が必要となります。Tireが要求されたときにStudlessTireのインスタンスを返すのか、SpikeTireのインスタンスを返すのか、それを設定します。Engineも同様です。
DIコンテナを利用すると以下のようなコードになります。
public class CarClient3{
public static void main(String[] args){
Container container = ContainerFactory.getContainer();
Car car = container.getInstance(Car.class);
}
}
このコードはCarClient2と全く同一です。インスタンスの依存性解決が行われる点は同じであるからです。例えばTireはStudlessTire、EngineはDieselEngineを設定によって選択した場合、そのインスタンスを包含するCarが生成されます。
ここまでは先の章と大差ないと感じるかもしれません。ここからが疎結合サポートの本領発揮です。
例えばStudlessTireからSpikeTireに取り替えたいときはどうすればよいでしょうか。答えは簡単です。単純に設定を変更すればよいのです。仮にテスト済みのコードがあっても、そのコードを修正する必要はないのです。
さらにEngineが何も完成していない状態であるとしましょう。そこでモックのEngineを作ってDIコンテナに設定して置けばよいのです。EngineがなくてもCarを取り合えず実装する事はできるのです。そして実装したCarのコードをテストすればCarは完成です。Engineが完成したらそのEngineをDIコンテナに設定し、正しいCarがクライアントに引き渡されるのです。
AOPのサポート
AOPとはAspect Oriented Programming(アスペクト指向プログラミング)の略です。書くと長くなるので、リンクだけ置いておきます。
http://ja.wikipedia.org/wiki/%E3%82%A2%E3%82%B9%E3%83%9A%E3%82%AF%E3%83%88%E6%8C%87%E5%90%91%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0
DIコンテナとAOPは全く関係ないのですが、DIコンテナは暗黙的に型を解決する機構を持つ為、AOPと相性がよいのです。
上記コードを元に説明しますが、CarはEngineのインスタンスに依存していますが、DIコンテナによってGasolineEngineのインスタンスが返される設定になっているとします。そこでクライアントがEngineにターボ機能を追加するケースを考えてみましょう。
クライアントがEngineにターボ機能を追加しようとする場合、DIコンテナなしではEngineの実装を改造するか、AOPライブラリを使用してターボをサポートするようにコードを書き換えます。これはテスト済みのコードを改造するというリスクが生じます。
しかしDIコンテナとコラボレーションすることによって、DIコンテナは自動的にGasolineEngineのクラスを拡張してターボ機能を追加します。クライアントはAOPの設定を行うだけでコードの修正を行う必要はないのです。
適用例
CarとかEngineとか・・・抽象的過ぎてイメージしにくいかもしません。そこで実際のアプリに置き換えてみましょう。
まずは、データアクセスオブジェクトとビジネスロジックです。
public interface EmployeeLogic{
Employee[] getEmployees();
}
public interface EmployeeDAO{
Employee[] select(String sql);
}
public class EmployeeDAOImpl implements EmployeeDAO{
public Employee[] select(String sql){
//SQL Execute...
}
}
public class EmployeeLogicImpl implements EmployeeLogic{
private EmployeeDAO employeeDAO;
public EmployeeLogicImpl(EmployeeDAO employeeDAO){
this.employeeDAO = employeeDAO;
}
public Employee[] getEmployees(){
return employeeDAO.select("select * from Employee");
}
}
そしてクライアントコード。
public class Client{
private EnployeeLogic logic;
public Client(EmployeeLogic logic){
this.logic = logic;
}
public void process(){
Employee[] employees = logic.getEmployees();
for(Employee employee : employees){
System.out.println(employee);
}
}
}
クライアントコードはビジネスロジックだけに関心を持てばよく、ビジネスロジックが完成していなければモックで代用します。ある程度完成した直後にSQLをログ出力したいとなった場合、EmployeeDAOに対してAOPでログ用のインタセプタを設定します。クライアントのコードはその間一切修正する必要はありません。
DIコンテナの実装
DIコンテナの中身はどうなっているのでしょうか。それは各DIコンテナで全然違いますが、(当たり前ですが)考え方は概ね似ています。
- 設定フェーズでクラスを集める
- 要求があったときにインスタンスを生成する
- クラスがロードされていなければクラスをロードする
- AOPの利用が必要な場合、クラスを拡張してロードする
- インスタンス生成時に必要な依存クラスのインスタンスを生成する(再帰的に2へ)
- その依存インスタンスを利用してインスタンスを生成する
- プロパティの型を走査して、プロパティの型に該当するインスタンスを生成する(再帰的に2へ)
- プロパティに生成したインスタンスを設定する
- クライアントへインスタンスを返す
という流れになります。インスタンスにはライフサイクルの概念があって、インスタンスが再利用される場合は生成及び設定のフェーズがスキップされます。よくあるライフサイクルは、
- シングルトン(一意)
- プロトタイプ(都度生成)
- リクエスト(APサーバのリクエスト)
- セッション(APサーバのセッション)
などです。(DIコンテナによってはスコープと呼ばれたりもします。)
さっきから「設定」という言葉が度々登場していますが、よくあるパターンとしてはXML形式の設定ファイルが利用されますが、Googleで開発されたGuiceというDIコンテナみたいに設定ファイルを利用しないものもあります。
最後に長くなりましたが、DIコンテナは.NETにもありますが、Javaでは比較的メジャーな存在になっています。いまではDIコンテナなしの開発は考えられないくらいに浸透しつつあります。新規で開発する機会がありましたら、一度触れてみてはいかがでしょうか。