Dependency Injection


BEAR.Sundayではオブジェクトが必要とするインスタンスを、自ら取得しないでインジェクターに代入してもらうことを期待します。

コンストラクタやセッターメソッド経由で外部から代入されることをインジェクション(注入)、必要とするものをディペンデンシー(依存)と呼びます。ディペンデンシーを利用するクラスはコンシュマーと呼びます。

特に特定タスクを担当しオブジェクトがツールとして使うオブジェクトをサービスオブジェクトと呼びますが、依存はサービスオブジェクトに限りません。DB接続オブジェクトや、配列やスカラー値などの値も含みます。

前回記事の挨拶リソースに戻りましょう。このリソースはメッセージを返す為に、$messageデータを必要としています。1

class Greetings extends AbstractObject
{
    $message = [
        'en' => 'Hello World',
        'ja' => 'Konichiwa Sekai',
        'es' => 'Hola Mundo'
    ];

    public function onGet($lang = en)
    {
        $greeting = $this->message[$lang];
        return $greeting;
    }
}

$messageデータをクラスに固定で持たないで、外部から代入するように変更してみましょう。
このようなセッターメソッドが必要です。

/**
 * @Inject
 * @Name(“greeting_message”)
 */
public function setMessage(array $message)
{
    $this->message = $message;
}

固定されていたデータが外部から代入できるようになりました。

ディペンデンシーをセッターメソッド経由でインジェクションしてるので、これをセッターインジェクションと呼びます。 またコンストラクターにインジェクトするのをコンストラクターインジェクションと呼びます。BEAR.SundayはRai.DiというDIフレームワークを使っていますが、サポートするインジェクションはこの2つのみです。2

このインジェクションを行うのがディペンデンシーインジェクターです。インジェクターは決められたルールでオブジェクトのコンストラクションを行います。クラスをインスタンス化しディペンデンシーをインジェクトします。コンストラクション後に行われる初期化メソッドの呼び出しや、オブジェクト破棄の直前に呼ばれるメソッドの呼び出し予約など「オブジェクトライフサイクル」に関する設定も行います。BEAR.Sundayでは原則的に全てのオブジェクトの生成はこのディペンデンシー・インジェクターが行い、オブジェクトの中からディペンデンシーを取得することは推奨されません。

インジェクションポイントと@Inject

ユーザーがセッターメソッドに@Injectと注記(アノテート)することでRay.Diは『ここに依存の注入が必要だ』ということが分かります。この「外部からの代入を期待する部分」をインジェクションポイントと呼びます。

※注)アノテーションはDoctrine.CommonsのAnnotationを使用していて、このアノテーションを使うためのuse文が必要です。

use Ray\Di\Di\Inject;
use Ray\Di\Di\Named;

インジェクションポイントに実際に何をセットするかはモジュールで設定します。モジュールではインジェクションポイントとインスタンスをバインドします。バインドの方法はいくつかありますがここでは特定の名前をつけてインジェクションポイントを指定する方法を使用していています。

実際のモジュールのコードはこのようになります。

class AppModule extends AbstractModule
{
    protected function configure()
    {
        $message = [
            'en' => 'Hello World',
            'ja' => 'Konichiwa Sekai',
            'es' => 'Hola Mundo'
        ];
        $this->bind()->named(greeting_message)->toInsntance($message);
    }
}

インジェクターの生成とオブジェクトグラフ生成

インジェクターを使ってこのクラスにディペンデンシーをインジェクトしてインスタンスを取得します。

$injector = Injector::create([new AppModule]);
$injector->getInstance(name\space\Greeting);

モジュールにはインジェクションポイントに対してどのインスタンスを提供するかという、いわばアプリケーションの構成知識が凝縮されています。その構成知識を使ってインジェクターはオブジェクトのコンストラクション(生成、インジェクト、ライフサイクルのセット)を行いインスタンスを返します。

インジェクションの連鎖とオブジェクトグラフ

ディペンデンシーインジェクションは専用のライブラリを使わなくても、手動でも行う事ができます。

たとえばUserクラスはDbクラスのインスタンスが必要でDbクラスはDB接続情報の文字列が必要だとします。これを手動のインジェクトするためには例えばこのようなコードが必要でしょう。

$dsn = $_ENV['master_db'];
$db = new Db($dsn);
$user = new User($db);

一行だとこうです

$user = new User(new Db($_ENV['master_db']));

このコードで明らかなのは、依存に依存があればその「依存の依存」から先に用意して順番に次の依存に渡さなければならないことです。Userクラスを生成する前に、DBオブジェクトの生成が完了してる必要があります。DBオブジェクトを生成するにはDB接続情報を取得しておく必要があります。これは通常のプログラムではごく当たり前のことです。

ところがRay.DIはUserクラスを作る前にDbオブジェクトを予め作っておく必要はありません。オブジェクトの構成知識を知っているインジェクターは必要な依存を遡って生成、インジェクトしてオブジェクトグラフをコンストラクションします。

例をあげます。

Userクラスを生成するときにRay.DiはそのクラスをつくるためにはDbオブジェクトが必要だと言う事を検知します。Dbオブジェクトを生成しようとしますが、ところがその生成にはDB接続情報が必要とも検知します。インジェクターがもつ構成知識でDB接続情報は得られます。得られた情報を使ってDBオブジェクトを生成します。そうやって依存の依存を順番に辿り依存性の解決(Dependency Resolution)を行い元のインスタンスを生成します。依存がツリー構造になっているこのオブジェクトをオブジェクトグラフと呼びます。

コンストラクションの再利用

BEAR.Sundayではプログラムの中でいつでもインジェクターを使い必要なインスタンスをオンデマンドで生成できますが、実際にはほとんどその出番はありません。boot時のルートオブジェクトグラフ(ページリソースやリソースクライアント)が生成される時に必要なオブジェクト、またはファクトリーの生成が全て完了するからです。3

前バージョンBEAR.Saturday (2008年)ではアプリケーションスクリプトからnew演算子を取り除くことが一つの目標でしたが、BEAR.Sundayではフレームワークサイドでもnewの利用はほとんどありあません。オブジェクトはコンストラクションされるとAPCのストレージに格納され、リクエストをまたいで再利用されます。

つまり現在のBEAR.Sundayではリクエスト毎に異なった処理をコンストラクタで記述することはできません。この制約はメリットとデメリットがあります。オブジェクトシリアライズ前提のためシリアライズできないクロージャや組み込みオブジェクトをコンストラクション時にプロパティにセットできません。一方、固定化されたオブジェクトグラフとより安定したフロー、強力で容易なキャッシュ機構、アノテーションやDI、AOP等を採用しながらも維持している強力なパフォーマンス等は大きなメリットです。

再利用はオブジェクトグラフの膨大な取得コスト4 を最小限にします。30,000を超えるindexページのファンクションコールは500以下になり、実行速度は数十倍になっています。

オブジェクトのコンストラクタは基本的にサービス開始の最初の1リクエストしか通りません。その特徴をv0.1.0alphaインストールの時に用意されるindexページでみてます。

indexページ画面

indexページリソーススクリプト
class Index extends Page
{
    use ResourceInject;
    public function __construct()
    {
        $this['greeting'] =Hello, BEAR.Sunday.;
        $this['version'] = [
            'php'  => phpversion(),
            'BEAR' => Framework::VERSION
        ];
        $this['extentions'] = [
            'apc'  => extension_loaded('apc') ? phpversion('apc') : 'n/a',
            'memcache'  => extension_loaded('memcache') ? phpversion('memcache') : 'n/a',
            'mysqlnd'  => extension_loaded('mysqlnd') ? phpversion('mysqlnd') : 'n/a',
            'pdo_sqlite'  => extension_loaded('pdo_sqlite') ? phpversion('pdo_sqlite') : 'n/a',
            'Xdebug'  => extension_loaded('Xdebug') ? phpversion('Xdebug') : 'n/a',
            'xhprof' => extension_loaded('xhprof') ? phpversion('xhprof') : 'n/a'
        ];
    }
    /**
     * Get
     */
    public function onGet()
    {
        $cache = apc_cache_info(user);
        $this['apc'] = [
           'total' => $cache['num_entries'],
           size => $cache['mem_size']
        ];
        // page / sec
        $this['performance'] = $this->resource->get->uri(app://self/performance)->request();
        return $this;
    }
}

トレイトを使ったセッターインジェクション

違うクラスでも求める依存が同じなら、同じセッターが利用できます。セッターメソッドはクラスをまたいで横断的に再利用できます。モジュールに新たな設定はありません。 メソッドの横断的利用、PHP5.4の新機能のtraitにすると便利で表記も簡潔になります。

use Ray\Di\Di\Inject;
use Ray\Di\Di\Named;
trait MessageInject
{
  private $message;
  /**
   * @Inject
   * @Name(“greeting_message”)
   */
  public function setMessage(array $message)
  {
      $this->message = $message;
  }
}

アノテーションのuse文も入っているので、利用クラスでは簡単にインジェクションが表記できます。まとめるとこうなります。

class Greetings extends AbstractObject
{
    use MessageInject;

    public function onGet($lang = en)
    {
        $greeting = $this->message[$lang];
        return $greeting;
    }
}

ボイラープレートになりがちなセッターインジェクションコードがスッキリ一行になり、依存関係を記すセルフドキュメントのようになってるのではないでしょうか。

プロバイダー6

Day.DiによるDIは次の2つのパートで成り立っています。

  • モジュールでのオブジェクトコンストラクション(Construction
  • コンシュマーでの利用(Execution

コンストラクションはモジュールで完了させ、コンシュマーでは利用だけを行います。 肝心なのはコンストラクションとエクスキューションを完全に分離して混ぜないことです。コンシュマーにディペンシーインジェクターやサービスコンテナを渡したりする事は推奨されません。78 ンシュマー内で新しいインスタンスが都度欲しい時はプロバイダーを使います。プロバイダーは最小のファクトリーで、引き数なしのget()というメソッドだけを持ちます。たとえばテンポラリーファイルのハンドルが都度、複数欲しいなら(Provider)$tmpFilePorviderをインジェクトしてもらって、このように使います。

$tmpFile1 = $this->tmpFileProvider->get();
$tmpFile2 = $this->tmpFileProvider->get();

プロバイダーはどのようにインスタンスを作るかという知識を全て持っています。引き数は渡す事ができず、最小化されたこのファクトリーに伝える事ができるのは生成のタイミングだけです。 なお、newは絶対に使っていけないというわけではありません。ごく小さなオブジェクトやPHPの組み込みオブジェクトなどは問題ないでしょう。

コンテナ?

Ray.Diでは内部でライフサイクル(シングルトンなど)の管理にオブジェクトコンテナを使いますが、依存性の解決には使いません。ユーザーがコンテナを触る必要も出番もほとんどありません。依存性は原則インターフェイスによって解決されます。SOLIDのD、 依存関係逆転の原則(DIP:Dependency Inversion Principle)です。

bootstrapのみで出現

このディペンデンシーインジェクターは通常、bootstrapスクリプト時にルートとなるアプリケーションオブジェクトを取得するのみです。ランタイムではオブジェクトの生成は原則行いません。providerPHPのcloneで複製をつくります。

Conclusion

この記事では、BEAR.Sundayアプリケーションの役割と働きをみてきました。Ray.DiはGoogle Guiceのクローンです。9 全ての機能が移植されてるわけではありませんが、ここで紹介した機能以外にも沢山の機能があります。Doctrine.Commons.Annotationライブラリを使い、AuraというPHP5.4フレームワークのAura.Diライブラリを拡張して作成しています。またRay.Diインジェクター自身の依存も手動でインジェクトされ拡張可能です。

Ray.Di – Guice style annotation-driven dependency injection framework for PHP

インジェクションポイントとディペンデンシーのバイディングはモジュールで設定し、インジェクターはどのオプジェクとが求められればどのインスタンスを渡すかという知識を持っています。モジュールはクラスをどのようにコンストラクトするかではなく求められた依存に対してどのインスタンスを渡すかという設定が行われています。そのため同じ依存を要求する違うクラスに新たな設定は必要なく、セッターメソッドのtraitを使ってより簡素な記述でディペンデンシーが取得できます。

Ray.Diの特徴はオブジェクトの生成と利用が完全に分離されていること10、モジュールでのDSLによるバインディング、APCを使ったオブジェクトグラフコンストラクションの再利用等です。11

Ray.Diは可変点の明確化と最小化というBEAR.Sundayのアーキテクチャ全体を通しての原則を支持します。

またRay.Diはモジュールで特定メソッドの実行にインターセプターをバインドすることが可能で、アスペクト指向プログラミングが利用できます。BEAR.Sundayではフレームワークやアプリケーションの動作や役割を様々なアスペクトの集合だと考えています。次回の記事ではAOPをサポートすRay.AopのBEAR.Sundayでの役割、アスペクト指向デザイン(AOD)により実装されたアプリケーション機能を紹介します。

はてなブックマーク - Object Framework – Ray.Di


<ol class="footnotes">

  • つまりこのリソースは$messageデータに依存しています []
  • プロパティにインジェクトするプロパティインジェクションはサポートされません。 []
  • 現在のBEAR.Sundayで登場する独立したオブジェクトはアプリケーションはappリソースを除くと基本的には3つしかありません。アプリケーションとリソースクライアントとページリソースです。その他のオブジェクトをそれらの「ルートオブジェクト」を構成するプロパティでしかない場合がほとんどです。 []
  • アノテーションが必要とするコメント文のパースだけでなくアノテーションの名前解決のためのPHPスクリプトのパースも行われてます []
  • 例えばYAMLファイルのCSVファイルのパースなどをコンストラクタで行いコンテンツとしてセットすると再利用されるので個別にキャッシュしたりする必要がありません。 []
  • このセクションはstackoverflowの記事を元にしています。http://stackoverflow.com/questions/2504798/dependency-injection-in-constructors []
  • コンシュマーがインジェクターを使ってサービスを取得することは、デメテルの法則(最小知識の法則)に違反します。 []
  • 例外はそのコンシュマーがファクトリークラスの場合です。BEAR.Sundayで唯一インジェクターを依存として受け取りオブジェクトコンストラクションをしてるのはリソースのnewInstance()メソッドです。 []
  • Guiceが使われているGoogleの代表的なプロダクトにAdSenseがあります []
  • 利用クラスでサービスコンテナへの依存がない []
  • コンストラクションは常にキャッシュされ、再利用されることを考慮したコーディングが必要です。 []
  • </ol>