2007年4月 7日

Dependency Injection の基本的なアイディア

Inversion of Control コンテナと Dependency Injection パターンを読みました。関連する事柄を広くカバーした、隙のない記事です。

ただ、割とボリュームがあるので、「Dependency Injection って結局何なの?」ということを手っ取り早く知りたい向きにはあまり向かないかもしれません。そこで、基本的なアイディアを手短にまとめてみました。

 

Dependency Injection (依存性注入、DIと略) とはその名の通り、依存性を注入するパターン (テクニック) です。もう少し言葉を加えると、依存性を内部に抱え込まずに外部から注入する、パターンです。

Dependency Injection の基本的なアイディアは「依存性を外部から注入する」です。 DIコンテナと呼ばれるフレームワークはいろいろありますが、特にそういったものを使わなくても DI は実践できます。

次の Ruby のコードを見てみましょう。

class SearchEngine
  def initialize
    @index = RemoteIndex.new("index.example.com")
  end

  def search(query)
    ... @index を使ってあれこれやる ...
  end
  ...
end

このクラスではコンストラクタの中で RemoteIndex (ネットワーク経由のインデックス) のインスタンス @index を作っています。このクラスをテストするには index.example.com にサーバを立ち上げて置く必要があり、ユニットテストが厄介なことになります。

そこで、RemoteIndex へのの「依存性」を取り除いてやる必要があります。コンストラクタを次のように修正すれば RemoteIndex への依存性を外に追い出すことができます。

class SearchEngine
  def initialize(index)
    @index = index;
  end
end

このようにコンストラクタを使って依存性を注入することを Constructor Injection と呼びます。ユニットテストのコードでは RemoteIndex と互換のインタフェースを持つ MockIndex (テスト用のはりぼてインデックス) のインスタンスを渡してやれば、ローカル環境だけでユニットテストを実行できるようになります。また、この変更により、 LocalIndex などの別の実装を渡してやることができるようになり、柔軟性が増しました。

ただし、変更以前のコードでは、クライアントは SearchEngine をインスタンス化するだけで済んだのに対し、変更後のコードでは、クライアントは RemoteIndex (あるいは他のインデックス) も事前にインスタンス化してやる必要があります。これらの生成の処理をまとめて面倒をみてくれるのが DIコンテナです。

ただ、上のコードくらい単純なものであれば、DIコンテナを使わずにファクトリメソッドを用意するだけで十分こと足ります。

def createSearchEngine
  index = RemoteIndex.new("index.example.com")
  return SearchEngine.new(index)
end

レガシーC でも DI

この「依存性を注入する」という考え方自体は、オブジェクト指向とは無縁の、レガシーな C プログラムにも応用できます。次のコードを見てみましょう。


int init_data() {
   Data *data = get_data();  // 何らかの方法で data を取得
   ... data を使ってあれこれやる ...
}

ここで、get_data() 関数が、ディスクから巨大なファイルを読み出していると、 init_data() 関数をユニットテストするのは困難になります。実行に時間がかかるユニットテストは小まめに実行できないためです。

そこで、この get_data() への「依存性」を取り除いてやります。一番簡単な方法は、init_data() の呼び出し側でデータを作って、 init_data(data) のようにデータを渡す方法です。 しかし、何らかの事情によりそれができない場合は、関数ポインタを使って関数への依存性を外から注入することもできます。

int init_data(Data *(*get_data_func)(const char*)) {
   Data *data = get_data_func();  // 関数ポインタ経由で呼ぶ
   ... data を使ってあれこれやる ...
}

こうしておけば、 init_data() の呼び出し側で、テスト用の軽い get_data() 互換の関数 mock_get_data () へのポインタを渡してやれば、さくさくとユニットテストを実行できるようになります。

まとめ

Dependency Injection の基本的なアイディアについて紹介しました。DI という言葉は、ややこしそうな DIコンテナのフレームワークと一緒に登場することが多く、何やらものものしい雰囲気が漂っていますが、基本的なアイディアは「依存性を外部から注入する」といういたって単純なものです。

より詳しくは Inversion of Control コンテナと Dependency Injection パターン などの文献を参照してください。