Zend_Application(2) /Zend FrameworkにおけるDIコンテナ活用のメリットについて/

PHPでのDIコンテナのわかりやすい説明としては「最小のDIコンテナ in PHP」や「DIコンテナなんていらない」の中で説明されているし、DIコンテナをより詳細に推し進めた形としてはSeasar2のドキュメントか何かを見ていただいた方がいいと思います。DIコンテナは使いこまれた技術で基本概念や実装方法については知られていますので、ここでは、ZFを例にしてDIコンテナを使うメリットについて具体的に検討してみたいと思います。

Zend Frameworkを例にとると

前記事Zend_Application(1) - noopな日々で触れた下記のページにあるように、
http://www.infoq.com/jp/articles/drinking-your-guice-too-quickly
DIコンテナはサービスロケーターから進化する流れがわかりやすいわけですが、それらを前提的な話として、ここでは3点に絞りたいと思います。

  • サービスロケータとしてのfrontController vs DIコンテナ
  • モジュール別ブートストラップを持つことのメリット
  • 結合テストがやりやすい

サービスロケータとしてのfrontControllerは割といいよ

ZFではフロントコントローラーをサービスロケーターとして使っているケースがあります。たとえば名前解決やらルート解決をしていくのに、ルーターやディスパッチャーへのリソースが必要な時、

<?php
$front = Zend_Controller_Front::getInstance();
$front->getDispatcher();
$front->getRouter();

などの方法によってフロントコントローラーのインスタンスから取得していることがわかります。
これはシングルトンパラダイムでリソースを取得するのにクラス決めうちで

<?php
$acl = My_ACL::getInstance();

みたいな形で直接シングルトンを呼んでしまうよりは

<?php
$acl = $front->getAcl();

という風にしたほうがマシなので、よいアプローチだと思います。どうましかというと、グローバルな依存対象が各シングルトンに分散しているよりは、一か所に集中していれば管理しやすいよね。そうすればそもそもシングルトンいらないし。そこにオブジェクト、もしくはオブジェクトの取得方法を集めておこうよっていうことですね。その意味Zend_Registryでもいいんですが、いかんせんキーに制約がなさすぎて余計な検証とか規約を作らなければいけないのがどうかなと。
これはフロントコントローラーのインスタンスをサービスロケーターとして参照している例になります。実際、ZFでのMVCのひな形ではフロントコントローラーパターンですので、ファサードとしてフロントコントローラーを使うのは自然な流れだと思います。
ただ、Zend_Controller_Front::getInstance()は継承された方のクラスのインスタンスを返すので依存性べったりっていうわけではないですが、infoqのところで言及されているように、フロントを参照しているコンポーネントがフロントコントローラーという概念に依存しているのは事実です。そうは言っても、いきなり全部DIコンテナに書き換えるというのは不毛な感じなので、フロントコントローラーを拡張してサービスロケーターとして各種オブジェクトを提供できるように実装しておくというのはよいプラクティスの一つだと思います。*1

サービスロケーター vs DIコンテナ

某所でサービスロケーターとDIコンテナの違いについて興味深いコメントを見たのですが、この違いはどちらかというとクライアントからみたアーキテクチャ上の話だと思います。そこでサービスロケータとDIコンテナの概念的な比較についてサンプルコードを出しておきたいと思います。ここではクライアントをクラスじゃなくて関数で定義します

  • hoge 依存先のオブジェクト
  • locater サービスロケータオブジェクト
  • container DIコンテナオブジェクト

クライアントの目的は、hogeインターフェースのオブジェクトのexecを実行することです。できれば設定でクラスやオプションを変えたいですよね。

<?php
//素の外部参照
function clientA() {
 global $hoge;
 if (! $hoge instanceof Hoge) throw new Exception('invalid hoge');
 $hoge->exec();
}

//サービスロケーター
function clientB() {
 global $locater;
 if (! $locater instanceof Locater) throw new Exception('invalid locater');
 $dep = $locater->get('hoge');
 $dep->exec();
}

//DIコンテナー
function clientC(Container $container) {
 $dep = $container->get('hoge');
 $dep->exec();
}

hogeインターフェースに依存しているクライアント[A-C]が

  • 自前でhogeオブジェクトを直接取得する
  • 自前で外部のサービスロケーターからオブジェクトを取得する
  • 注入されたDIコンテナから取得する

の違いです。関数の引数で注入って話は聞いたことがないですが、引数の部分をコンストラクタ引数でもセッターにでも脳内変換してみてください。
サービスロケーターパターンでは、hogeオブジェクトを直接globalから取得するよりはマシですが、$locaterが適切な名前で外部に存在することを実装時に保証しなければなりません。
サービスロケータでも当面の間は良好にシステムを組めるわけですが、たとえばサービスロケーターとしてのZend_Controller_Frontは、そこにぶら下がっているクラス群に遠慮して最適化が図れないようになってしまいます。逆にサービスロケーターに依存しているクラスは、同じサービスロケーターを持っているシステムにしか簡単に移植できないという問題をはらみます。たとえば、フロントコントローラーがバージョンアップすると別のコンポーネントも同時にバージョンアップしなければならないが、旧バージョンが別のコンポーネントに依存していてとかいうヘアボールを作ってしまいます。要するにカップリングの弊害ってやつです。ただ、サービスロケーターの実装変更自体は非常に緩やかにしか発生しないので、変更リスクが目に見えて大きくはならないという点でサービスロケーターへ依存することのデメリットを感じにくいのだと思います。
DIコンテナーパターンでは、$containerがhogeインターフェースのオブジェクトを返す仕様になっているので、それがどこで定義されようが関係なく独立性が高く、安全です。依存はコンテナに集約されるので、「ルースカップリングとタイトコヒージョン」が守られるます。たとえば、バージョンアップ問題についても、他のコンポーネントがバージョンアップされても、それに対応できるコンテナーさえ書けば、コンポーネント本体に手を入れなくても済みます。この点については、フロントコントローラーをロケーターにせず、独立したロケーターをシングルトン実装すればそこで吸収できるという反論もありうるのでサービスロケーターのネガティブポイントとは言い切れないのですが、コンポーネントからの外部参照があるかどうかという点において、DIコンテナーに軍配が上がると思います。

DIコンテナのアンチパターン

やや抽象的なコードで、DIコンテナ、マンセーって感じで書いてしまいましたが、当然DIコンテナについてもアンチパターンが存在します。2005年のこの記事DIコンテナの本当の使いどころ | 技術トピックス | ウルシステムズ株式会社がよくまとまっていました。
Zend FrameworkでDIコンテナを使うとすると、おそらくinjectorをどう実装するかっていうところで問題になると思います。たとえばActionControllerをDIコンテナでDIしようとすると、Dispatcher実装を変更しなければならないとか、その辺の解決をそこかしこで始めると、エイヤーで結局DIコンテナをシングルトンで解放とか。=>それってわかりにくいサービスロケーターだよね。っていう落ちになりがちです。
まぁ、モデルDIコンテナだったら、アクションヘルパーかなとか、実際的な話はできるとおもいますが、それもコンテナの分散を招くのでどうしたもんかなぁと。理論上よいからといって実際にいい実装を書けるかどうかは別の次元ってことかと。
ですので、理想論は理想論として理解しつつ、現実的なライン引きに関する考察も必要です。

モジュール別ブートストラップ

Zend_Applicationはモジュール別ブートストラップ機能を提供しています。
単一のサービスでデフォルトモジュールだけで運用しているようなシステムであれば関係ないのですが、複数のモジュールを別の開発元から供給してもらう時など、場合によってはデフォルトとは違う前提で動作していることがあります。使用するプラグインやフロントコントローラにセットするオプションとか。
そうなると、それらモジュールは同居させることが出来なくなってしまいます。それはもったいないですよね。かといって、デフォルトのMVCコンポーネントだけを前提にして動作するようにモジュールを作れと言ってしまったのでは、疎結合が泣きます。
そこで、DIコンテナとモジュール別ブートストラップの出番となるわけです。モジュールが起動される際にモジュール別のブートストラップが走ることにより、依存性を解決する。ブートストラップで必要なプラグインをロードしたりといった依存性を解決してコントローラーに注入できるようになっていれば、個々のモジュールが独立したまま同居できるようになるわけです。肝心なことは、疎結合で構成されたコンポーネントやモジュールのグルーになる部分をどう構成するか、それってやっぱりDIコンテナがわかりやすいよね?ということで使うわけですね。フレームワーク全体をDIコンテナマンセーで作るのとはだいぶ違います。
具体的な話に戻すと、やっぱりDispatcherで注入したいんですが、Dispatcherを変更しないでアクションヘルパーでやる方法もあり。
さて、そろそろうまい落とし所が見えてきそうです。ZendのMVCではモジュールが境界となって、interモジュールなオブジェクトとinnerモジュールなオブジェクトに分けられるように思います。すると、interモジュールなオブジェクトについてはグローバルなDIコンテナに集約してモジュールに注入する、innerモジュールなオブジェクトはインスタンスを内部で適切に受け渡すという実装案がよさそうです。
こうしておくと、実際にはinnerモジュールなオブジェクトであっても、interモジュールになる可能性のあるクライテリアについてはDIコンテナに格納・取得した方がいいんじゃない?って話がでると思いますが、そこは両方できるように検討しておけばいいと思います。
そのぐらいが、設定地獄に陥らなくても済む、いい塩梅な気がします。

結合テストがやりやすい

さて、Zend Framworkではテストが徹底されているので品質がいいという話にはなっていますが、結合テスト自体はユーザー依存となっています。理由は結合方法が多彩でユーザー依存なのでフレームワークとして完全な結合テストを行うのは難しいためです。
DIコンテナを使うと結合をコンテナで表現することができるので、モックオブジェクトを使った結合テスト等、システム開発のより早い段階から結合テストを行うことができるようになります。
実際、結合テストを長くとりすぎるプロジェクト計画は、自信なさげに見えますが、実際に不具合が生じるのは結合テスト以降であることもあるので、各単体が組み上がってからでないと結合テストができないような外部依存はなるべく削除しておいた方がいいわけです。
モジュールを外部発注する場合やチームを分ける場合なども、コンテナベースでテストができれば品質保証されたモジュールを調達しやすいということになります。
ここでは、モジュール単位でのDIコンテナを検討したのですが、より大きな枠組み、小さな枠組みを選ぶかは設計次第なので自由に選択するといいと思います。

まとめると

DIコンテナとは何かとかどういう実装がDIコンテナかということよりも、どのように使うとどんなメリットがあるのかという具体性を考慮して使いどころを見極めることが重要と考えます。
その意味でZend_Applicationが提供するグルーが気に入るかどうか、目的に合致しているかどうかを見極めて、自分なりのグルーを作りだせばいいってことだろうと思います。

*1:その拡張クラスへの依存を他のオブジェクトに持たせるという副作用を容認するということですが