図1●コンポーネント間の依存性
オブジェクトが他のオブジェクトを利用する場合,最も単純な方法がオブジェクトを生成したり,呼び出したりするコードを直接記述するやり方だ。これは,両者を結ぶ釘の頭が呼び出す側のオブジェクトに入り込んでいる状態と言える。利用される側のオブジェクトは釘から容易に抜けるが,頭が入っているオブジェクトの再利用は難しい。両方の釘を抜けるようにするには,何らかの媒介する仕組みが必要である。
図2●Dependency Injection(DI)
JavaやC#には,クラスの階層構造と関係なく,共通のメソッドを定義するための「interface(インタフェース)」という仕組みがある。通常,あるインタフェース(interfaceX)を実装したオブジェクトを利用したいクラス(classA)は,そのインタフェースを実装したクラス(classB)を指定し,生成する必要がある(a)。これではclassAはclassBに依存してしまう。コンパイル時にclassBを必要とする。interfaceXを実装した他のクラスに入れ替える際にはコードを書き換える必要もある。この問題を解決するために,オブジェクトの生成処理をコンテナなどオブジェクトの外部に任せるのがDIの考え方である(b)。classAにはclassBを生成したり呼び出したりするコードが混入せず,interfaceXを指定するだけでよい。コンパイル時にはclassBの実装は必要ないし,実行時にinterfaceXの実装クラスを動的に入れ替えられる。
図3●Seasarの仕組みと利用例
Message,Nameという二つのインタフェースと,それぞれの実装クラスを別々のパッケージで定義した。実装クラスは.diconファイルに指定しておき,これをSeasar2に読み込ませる((1))。次に,インタフェースを指定してコンテナにオブジェクトを要求する((2))。コンテナは.diconファイルを基に,オブジェクトの生成やオブジェクト同士の結びつけをする((3))。つまり依存性の注入を実行する。注入の方法はいくつかあるが,ここではコンストラクタによる注入とsetter(値を設定するメソッド)による注入を使っている。
 オブジェクト指向は,システムを部品化するのに適した手法である。だが「オブジェクト指向は部品同士を結合させる手段としては強力でない。部品を自由に結合したり離したりするのは,実はなかなか難しい」(東京工業大学大学院情報理工学研究科数理・計算科学専攻の千葉滋助教授)。

 部品Aが部品Bを利用する場合を考えてみよう。部品Aのソースコードには通常,部品Bを利用するための記述が入り込む。これは「部品の間に釘がささった状態」(千葉氏)に例えることができる(図1[拡大表示])。部品Bだけをはずして別の場面で使うのは容易だが,部品Aには釘の頭の部分が残っている。これを無理に抜こうとすると,部品の一部が崩れてしまう。つまり「部品Aを単独で再利用しようとすれば,ソースコードを修正するしかない」(千葉氏)のである。この問題は,部品を再利用する際の足かせとなる。保守性の低下にもつながる。

 こうした問題を解決するものとして,現在注目されている技術が二つある。一つはDI(Dependency Injection)*1。「依存性の注入」と訳される技術だ。もう一つはアスペクト指向。オブジェクト以外のシステム分割の軸を採り入れて,部品間の依存性をなくす。

DI(Dependency Injection)
依存性を外部で管理

 DIは,J2EE(Java2 Platform,Enterprise Edition)の開発者の間で広まった考え方だ。「Spring Framework」や「PicoContainer」が,DIの考え方をいち早く採り入れたものとして知られている。日本では,国産の「Seasar2」も人気を集めている。

 DIは,部品間の依存性を部品以外の場所で管理することにより,部品そのものから依存性を排除しようとする考え方である。依存性は,コンポーネントを管理するミドルウェアである「DIコンテナ」が処理を行う。DIコンテナが各部品の実体を生成し,それぞれの部品の依存関係を実行時に解決する。

 ある部品のクラス(classA)が,他の部品のクラス(classB)の機能を呼び出したいとする(図2[拡大表示])。その機能はインタフェース(interfaceX)で定義されている。つまりインタフェースは部品の「外部仕様」で,classBはその実装である。本来なら,インタフェースさえ分かっていれば,その実装を関知しなくていいというのが部品としてあるべき姿。しかし実際にはオブジェクトを生成するときに,実装であるclassBの情報が必要となる(図2-a)。classBのクラス名を指定してオブジェクトを生成しなければならないので,結果としてclassAはclassBに依存してしまう。

 DIでは,classAとclassBの仲介をDIコンテナに任せる。依存性の情報は設定ファイルに記述しておき,DIコンテナに読み込ませる(図2-b)。DIコンテナはこの情報に基づいてclassBのオブジェクトを生成し,classAのオブジェクトに渡す。

 この結果,classAにはclassBを直接指定するコードが混入しない。DIコンテナから受け取ったオブジェクトに対して,インタフェース名でアクセスすればよい。classAとclassBの間には依存性がなくなる。

生成処理をコンテナが肩代わり

 Seasar2を使って,DIの動作の実際を見てみよう。一連の処理を示したのが次ページの図3[拡大表示]である。アプリケーションの開発者はまず,Seasar2の設定ファイルである「.dicon」ファイルをSeasar2に読み込ませ,コンテナを生成する。 .diconファイルに記述されているのは,コンテナで動作させるクラス(コンポーネント)の情報である。ここでは「message.MessageImpl」と「name. NameImpl」という二つのクラスのコンポーネントを登録している。またオブジェクトの生成時に渡す引数の指定も行っている。

 アプリケーションがコンテナに対してオブジェクトを要求すると,コンテナが条件に合うオブジェクトを自動的に生成する。ここでは,Messageインタフェースを持つオブジェクトを指定している。

 登録されたコンポーネントのうち,Messageインタフェースを実装しているのはMessageImplなので,コンテナはそのオブジェクトを生成する。.diconファイルで引数を指定しているので,引数付きのコンストラクタを呼び出す。

 MessageImplでは,Nameインタフェースのオブジェクトを使用する。 .diconファイルにはNameを実装したNameImplが登録されているので,コンテナはそのオブジェクトを生成し,MessageImplクラスのオブジェクトに渡す。

 ここで注目すべきは,オブジェクトを生成する「new」というキーワードがソースコード中のどこにもないことである。この結果,どのクラスにも,他のクラスの実装に関する情報が混入しない。すべて外部仕様であるインタフェースだけで完結している。

出典:2005年5月号 32ページ
記事は執筆時の情報に基づいており、現在では異なる場合があります。