読者です 読者をやめる 読者になる 読者になる

DI(Dependency Injection)とは - 第2回

第1回では、DIパターンについて説明しました。今回は、DIパターンを踏まえて、DIコンテナとは何かについて説明します。

DIコンテナとは何か?

前回、DIパターンは、「依存性をクラスの外に出し、依存性の実装クラスを交換可能にするパターン」であることを説明した。このパターンを適用する事により、依存性のオブジェクトを生成し、その依存性を注入する必要がでてきます。しかし、依存性の注入を、そのクラスの利用者にさせた場合、その利用者は依存性を注入するのに、その依存性の実装に依存してしまいます。そこでDIコンテナを使います。

DIコンテナは何をしてくれるとかというと、

  • 依存性のオブジェクトを生成
  • 依存性のオブジェクトを注入

をして、サービスオブジェクトを組み立てるということをしてくれます。DIコンテナは名前の通り、DIするコンポーネントのRegistryなわけですが、ただのRegistryでは無く、このようにサービスオブジェクトを組み立てる役割を担います。

例えば、A-->B-->Cという依存関係があった場合、コンテナからAのオブジェクトを取得すると、コンテナは「AにBのオブジェクトが設定され、BにCのオブジェクトが設定されたAのオブジェクトを生成して返す」ということをします。

Aサービス取得のイメージとしては、以下のように書きます。

Container->get('A');

アプリケーション全体のオブジェクトの生成をDIコンテナに任せる事で、サービスの依存性はサービスの利用者、すなわち、DIコンテナの設定にのみに依存する事になります。要するに、サービスからは実装の依存性が消えるということになり、DIコンテナの設定を切り替えるだけで依存性を切り替えることができるようになります。

サービスクラスからは、実装への依存性が消えることになるので、そのクラスが単体テストができるようになるということになります。

これによって、外部環境への依存からの脱却、例えば、Memcached立ち上げないとテストができない、Catalystがないと単体テストができない、DBがないとクラスの単体テストができない、などのuntestableな状態から脱却できるようになります。

依存関係解決の方法

DIコンテナの依存関係の解決方法には、以下の二つの方法がよく採用されています。

  • auto wiring
  • manual wiring

型に応じて自動でオブジェクトの依存関係を設定するのがauto wiring、manual wiringは設定ファイルによるwiringです。ちなみに、wiringというのは、オブジェクトの依存関係を設定するという意味の用語です。

フレームワークによって、auto wiring と convention を使ったりなど、設定方法にはフレームワーク毎に複数の種類の組み合わせが行われています。

DIコンテナが生成するサービス(オブジェクト)の生成単位

取得できるサービスの生成単位は、多くのDIコンテナでは以下の2種類が選択できます。prototypeは、取得時に毎回生成、singletonはコンテナ内で1つのみ生成です。

サービス(オブジェクト)の生成単位

  • prototype
  • singleton

Webアプリケーションで使う際は、singletonで使う事が多いです。インスタンスはWebアプリケーションをデプロイしたときに1インスタンスだけ生成して使い回すという形にします。

余談ですが、「オブジェクト生成」がDIコンテナの役割となることで、DIコンテナを使う開発では、以下のパターンを使う事が殆どありません。

  • factory
  • prototype
  • singoleton

Bread::BoardによるDIコンテナの使用例

PerlのDIコンテナには、Bread::Boardがあります。Mooseで有名なstevan作のDIコンテナです。ここでは、Bread::Boardを利用したDIの例を説明します。

MyApplication -> logger といった依存関係があるものとします。ここでは、loggerには、FileLoggerのオブジェクトが注入される形になります。

package MyApplication;
use Moose;
has 'logger' => (
    is =>'ro',
    required => 1
);

sub run {
    my $self = shift;
    $self->logger->log();
}

1;
package FileLogger;
use Moose;
has 'log_file_name' => (
    is=> 'ro',
    required => 1
);

sub log {
    my $self = shift;
    print "log\n";
}

1;

以下は、Bread::Boardによるコンテナのコードです。applicationサービスはMyApplication classで、applicationサービスは、loggerサービスに依存しているという形になっています。また、loggerサービスは、FileLoggerで定義されています。

my $c = container 'MyApp' => as {

    service 'log_file_name' => "logfile.log";

    service 'logger' => (
        class        => 'FileLogger',
        lifecycle    => 'Singleton',
        dependencies => [ depends_on('log_file_name'), ]
    );

    service 'application' => (
        class        => 'MyApplication',
        dependencies => {
            logger => depends_on('logger'),
        }
    );

};

$c->fetch('application')->get->run;

以下のように、applicationサービスを取得すると、MyApplicationのオブジェクトはFileLoggerオブジェクトが設定された状態(依存関係がwiringされた状態)で取得されます。そのため、MyApplicationのrunメソッドを実行すると、Loggerのlogメソッドが呼ばれます。

$c->fetch('application')->get->run;

この例で示したように、MyApplicationはFileLoggerには依存しておらずlogというメソッド(インターフェース)を持つオブジェクトにのみ依存しています。すなわち、固有のクラスには依存していません。

このようにDIコンテナが、MyApplicationの外から依存関係を注入してくれるため、各種サービスは依存関係のあるクラスの実装の詳細を知ることがないため、互いに疎に結合されます。それによって、各サービスは単体でテストができるようになります。

# catalyst groupで書いていたときの記事に少し加筆しました
http://catalyst.g.hatena.ne.jp/dann/20080402/1207148993

今回説明しなかったこと

DIコンテナのオブジェクト生成以外の役割として、Aspectの管理/適用という重要な役割があるのですが、今回は説明しませんでした。これについては、また別の機会に説明したいと思います。

次回は

ここまでで、大体DIコンテナのイメージができたのではないかと思います。次回以降は、WebアプリケーションフレームワークでのDIコンテナの使い方について説明したいと思っています。