Laravel4、IoCコンテナの魔術

タグ: Laravel4   テスト志向  

LaravelにはIoCコンテナが含まれています。

ただ、IoCコンテナはLaravelの十八番というわけでなく、この機能だけを提供するライブラリーも存在しています。他のフレームワークにも含まれています。

Laravel4で多少ややこしいのは、コンストラクタによる注入とコンテナから明確にインスタンスを取得する機能を提供しており、両者を同じものとしてドキュメントに記載してしまっていることです。これは異なった2つの機能だと分けて考えたほうが理解は簡単です。両方共通の目的は依存性の注入です。簡単に表現するなら、特定のクラスをハードコードすること無く、外部から指定できるようにするというものです。それにより、クラスの置き換えができるようになります。

なぜ置き換えられることが重要かと言えば、いざ変更が必要になった時に、最小限度のクラスの変更で済むようにです。できるだけ影響を最小限度に抑えるためです。

最近の傾向として、テストのしやすさが上がるため、この手法を取り入れることをすすめている情報がたくさんあります。

大前提:何を作るのか

IoCコンテナは「必ず使用しなくてはならない」ものではありません。どんな機能も、どんなパターンも本来使用する状況に適した場合に使用するものです。

ところが私達は誰かが声高に「こうするべきだ」なんて言っていると、それがWeb上のブログだろうと、ツイートだろうと、気にして、従ってしまいがちです。

ばしっと言い切るのは気分が良いし、なんとなく頭が良さそうに見てもらえるし、書いている人は権威的にみられるので、そう書く人達の気持ちもよく分かります。しかし、困ったことにその前提となる状況は通常書かれておらず、そのためこうしたエゴイスティックな記事は私達を惑わすことになります。

もし、サクッと小さなWebアプリを作成したり、使い捨てのツールを構築したり、プロトタイプを素早く完成させたいのでしたら、IoCコンテナ云々かんぬんは必要ありません。多分求められるのは開発スピードであり、メンテナンスやテストしやすさは必要ないでしょう。

IoCという主題は、私達がメンテナンスやテストというものに興味を抱いた時、必要となるものです。

コンストラクターを使用する

IoCコンテナを使用しなくてもIoCは実現できます。呼び出し側で使用するクラスのインスタンスを指定してあげればよいのです。

例えば、レポートを出力の生成クラスを考えましょう。目的は印刷したレポートです。プリンターはクラスのインスタンスとして渡します。

$printer = new OnLinePrinter();
$report = new Report($printer);
$report->print();

典型的なコンストラクターの注入パターンなら、次のようなコードになることでしょう。

class Report {

    protected $printer;

    public function __construct($printer) {
       $this->printer = $printer;
    }

    public function print() {
        // 何やらレポートを作成し、$reportに代入する
     $this->printer->output($report);
    }
}

コンストラクターで渡されたインスタンスをプロパティーに保存しておき、クラス内で必要な場合はそのプロパティーを利用するという「やり型」です。日本人の好きな「型にはめて」いるわけです。

この標準的なIoCのコンストラクターによる注入パターンをLaravelではこう書けます。

まず、呼び出し側です。

$report = App::make('Report');
$report->print();

そしてレポート生成クラスです。

class Report {

    protected $printer;

    public function __construct(OnLinePrinter $printer) {
       $this->printer = $printer;
    }

    public function print() {
        // 何やらレポートを作成し、$reportに代入する
     $this->printer->output($report);
    }
}

まず呼び出し側ですが、Reportという登録名でインスタンスをコンテナを利用し取得しようとします。この'Report'という名前について具体的なクラス名が指定されていたり、生成のためのロジックがクロージャーで指定されていた場合はそれを使います。

もし登録されていないのでしたら、登録名と同じ名前のクラスのインスタンスを生成します。

呼び出し側では、使用したいクラスがさらに別のどのクラスを使用するかを知っておく必要が無くなりました。呼び出されるクラスが何を使用するのかは、そのクラス自身が知っていればすむ情報です。呼び出し側で用意する必要が無くなり、そのクラスは使いやすくなります。クラスを呼び出すために準備をしてあげる手間が省けるわけです。

さて、呼び出されるレポートクラスを見てみましょう。'Report'という名前を解決したルールが各クラスのコンストラクターの引数にも適用されます。引数にヒントとしてクラス名を指定してある場合、LaravelはIoCコンテナを使用し、それを解決=取得しようとします。

今回の場合、'OnLinePrinter'という登録名が存在しているかまず調べ、存在しているなら登録されたクラス名のインスタンスを生成するか、クロージャーを実行し新スタンスを獲得します。登録名が未登録であれば、登録名と同じ名前のクラスのインスタンスを生成します。

OnLinePrinterに対して、事前に何も登録していません。ですからOnLinePrinterとクラスのインスタンスを生成し、それが$printerに渡されます。もし仮にOnLinePrinterというクラスのコンストラクターで、クラスヒントにより引数が指定されていれば、IoCにより同様のルールが適用され、インスタンスが生成されます。

この「未登録の名前に対して、その名前自身のクラスのインスタンスを生成しリターンする」というルールは大変便利です。IoC(DI)を徹底的に利用しようとするならば、作成するクラス全部に対して依存性の定義が必要になります。しかし、Laravel4のIoCコンテナを利用するならば:

  • 明示的なインスタンスの生成はnewの代わりにApp::makeメソッドを使用する
  • 自クラスが使用する他のクラスは、コンストラクターのタイプヒントを利用し指定する

という、手法を取れば、登録名と実際のクラスを結合させるための定義を全部、いちいち書かなくて済みます。

そして、他のクラスを使用する必要がある場合、たぶんそれはテスト時が一番多いでしょうが、その時は必要な登録名に対してのみ定義してあげれば良いのです。

// ロジックが必要であれば
App::bind('OnLinePrinter', function()
{
    // ロジックを書き、その中で
    // インスタンスをリターンします
    return new HiSpeedInkjet;
});

// 実際のクラスと結合すればよいだけの場合
App::bind('OnLinePrinter', 'HiSpeedInkjet');

多分IoCを取り入れていらっしゃる方なら、LaravelのIoCの便利さにピンと来られたでしょう。いかに簡単に手間なく依存性注入を適用できるのか気が付かれたと思います。

もし、Laravelを学習中でIoCについて勉強しようとされている方が読まれても、すぐにはピンと来ることはありません。多分このIoCという主題は中級クラス以上の方々に必要なものです。メンテで苦労したり、テストコードが素直に書けなくて悩んだりした経験があれば、便利さが実感できますが、そうでなければ単に面倒な手順に見えるのは仕方ありません。Laravelであろうと、何であろうとプログラムの経験を積んでいけば遅かれ早かれ理解できます。

大丈夫ですよ。このロートルでも数ヶ月で気がつくことができました。皆さんならもっと早く理解できます。それでもアドバイスをするならば、「どうやったらテストがしやすくなるか」という視点を持つことです。そしていろいろなテクニックやツールにチャレンジしてください。

コンテナを利用する

直接コンテナを利用するコードは既に使用してしまいました。App::make('登録名')をnewの代わりに使用します。

この使い方のIoCコンテナは自由にいろいろな場所でインスタンスを取得できます。ですからある意味制御構文の中のGOTO文のようなもので、やたらめったらに使用すべきではありません。

ですが、これを完全に否定してしまうのもいかがなものでしょう。Laravel4ではコンストラクターのタイプヒントによる注入が強力ですが、これを利用するにはapp::make()でインスタンスを取得する必要があります。またLaravel3では実装されていません。他のフレームワークでも同様でしょう。

例えばあなたがライブラリーを書いているとしましょう。もちろん、他の人に使ってもらうためです。いくら依存注入がメンテやテストのやりやすさをあげるとはいえ、それを利用者には押し付けられません。例えば…

$transpoter = new Transporter;
OneLib::DokodemoDoor('しずかちゃん家のお風呂場', $transpoter);

そしてメソッドの使い方としてこう記述します。「必ずトランスポーターのインスタンスを第2引数に渡してください。」

嫌ですね。こんなライブラリー使いたくありませんね。使用するのに準備が必要だなんて。でも実際にありますよね。:D

もちろん、必要な場合ならかまいません。でも、この場合の転送機はどこでもドア専用です。使用者が渡す必要はあまり感じられません。

「IoCコンテナはいけない。」は簡単に口に出来ます。しかし、余りにも一般的です。この場合、明確にしておくのは「誰が利用者」かです。利用者とはアプリの使用者であり、中のコードには全然関心を持たない人達であれば、内部の構造などどうでも良いでしょう。しっかりと動くことを期待されているだけで、メソッドの引数などには関心を持ちません。

ですがライブラリーの場合、利用者とはプログラマーです。簡単に使用できるように、手間なく、引数は少くメソッドを設計しましょう。そうでなければ、あなたのライブラリーは使われないでしょう。

どこでもドアを使うのなら、やっぱり行き先だけ指定するだけで使いたいものです。

OneLib::DokodemoDoor('しずかちゃん家のお風呂場');

インスタンスの取得もアチラコチラで行うのでなく、コンストラクターで行えば、うるさい方も満足するのではないでしょうか。

Class OneLib{

    protested $transpoter;

    public function __constract() {

        $this->transpoter = App::make('Transpoter');
    }

    DokodemoDoor($distonation) {

        $this->transpoter->go($distonation);

    }
}

私はインスタンスが一回しか利用されないのに、絶対コンストラクター内で呼びだせという原理主義者でありませんが、複数箇所で利用されるのでしたら、このようにコンストラクターで使うのを原則にしたほうがスマートであると思います。

インターフェイスのタイプヒント

コンストラクターによる注入に戻ります。Laravelでは具象クラスではなくインターフェイスもタイプヒントとして指定できます。

これはそのクラスが置き換え可能であることを明示的に表してくれます。

// 呼び出し側
$report = App::make('Report');
$report->printReport();

// インターフェイスの定義
interface Printer {
    public function output();
}

// 実際のプリンタークラスの定義
class OnLinePrinter implements Printer {
    public function output() {
        // 特定のプリンターへのプリント処理…
    }
}

// レポート
class Report {

    protected $printer;

    public function __construct(Printer $printer) {
       $this->printer = $printer;
    }

    public function printReport() {
        // 何やらレポートを作成し、$reportに代入する
        $this->printer->output($report);
    }
}

やはり、このほうが私にはぴったりきます。なにせプリンターはたくさんありますから、それを表すクラスもたくさん用意する必要があるでしょう。そして、それらは交換可能であることでしょう。

ただし、この場合自動的な解決はできません。インターフェイスはインスタンス化できないからです。そのインターフェイスを実装したクラスを指定する必要があります。

実行途中で変更する必要が無いのであれば、インターフェイス名に対して、具象クラスを指定します。

App::bind('Printer', 'OnLinePrinter');

実行途中で動的に切り替えられるようにするには、上記のbindメソッドで切り換えるか、Reportクラスに直接渡しましょう。

switch ( $printer ) {
    case 'OnLinePrinter' :
        // IoCコンテナで結合
        App::bind('Printer', 'OnLinePrinter');
        $report = App::make('Report');
    case '3D' :
        // インスタンスを直接渡す
        $printer = App::make('ThreeDimPrinter');
        $report = App::make('Report', array($printer));
    default : 
        // インスタンスを直接渡す。その2
        $report = App::make('Report', array( new DefaultPrinter ));
     // $report = new Report( new DefaultPrinter ) );
}

Appクラスのmakeメソッドでインスタンス化する場合、インスタンス化するクラスに渡す引数は配列で指定します。

まあ、最後の例はポリモルフィズムを使うのが、グッドプラクティスですね。そこまで説明できませんので、IoCの仕組みの便利さだけを、味わってください。後は、皆さんにお任せします。