Laravel4、依存注入とコンテナ(3)

タグ: Laravel4  

そこで、IoCコンテナ登場です。

IoCコンテナは、必要な時に必要な場所で、インスタンスを渡してくれる仕組みです。

ある名前(文字列)を指定した時、予め登録しておいたインスタンスを返す仕組みです。テストの時はモックなどを返すようにします。実際に使用する時、本当のクラスのオブジェクトを登録しておきます。

Laravelには最初から、備わっています。それを利用してみましょう。

まず、コードで確認してみましょう。Grape.phpです。

<?php

class Grape
{

    function get()
    {
        // いつでも、好きな時に呼び出せる。
        // けれど、依存関係が読み出しづらい。

        $water = App::make( 'Water' );
        $sun = App::make( 'Sun' );

        return $water->get() + $sun->get();
    }

}

この場合、App::makeメソッドに指定しているのは、クラス名ではありません。そこで必要なクラスの登録名、抽象名です。分かりやすい名前を付けてください。

でも、紛らわしいので、クラス名と一緒にしておきましょう。(すると、後で幸せになれます。)

どこか、アプリケーションの最初のほうで、どうやってインスタンス化するのか、指定しておきます。

App::bind( 'Water', function () { return new Water; } );
App::bind( 'Sun', function () { return new Sun; } );

この設定を使用し、IoCコンテナは、指定された登録名のインスタンスを生成するわけです。

クロージャーを使用していますので、今回のように単純にnewするだけでなく、もっと複雑な準備コードを用意することもできます。

しかし、たいていは登録名に対して、対応するクラスをnewするだけです。その場合、簡易記述バージョンが利用できます。

App::bind( 'Water', 'Water' );
App::bind( 'Sun', 'Sun' );

はい。できました。これだけです。2つ目のパラメーターは、本当のクラスを表す文字列に代わりました。便利です。

ところが、話はまだ続きます。なにせ、Laravelですからね。

最初のパラーメーターの登録名と、2つ目のパラメーターのクラス名が同じならば、登録を省略できます。つまり、何もしなくても、App::makeでクラス名を指定すれば、それをnewと同様にインスタンス化してくれます。

ほら、幸せになれました。さらに、ハッピーなことに、名前空間が付いているクラスも、そのまま名前空間付きで指定しておけば、インスタンス化できます。ただし、名前を区切るバックスラッシュを二重にするのをお忘れなく。

結局、App::bindにより、準備を何もしなくても、実際のコードである、Grapeクラスは動作します。

では、テストコードを見てみましょう。GrapeTest.phpです。

<?php

use Mockery as m;

class GrapeTest extends TestCase
{

    public function testGet()
    {
        $waterMock = m::mock( 'Water' );
        $waterMock->shouldReceive( 'get' )
            ->once()
            ->withNoArgs()
            ->andReturn( 50 );
        App::instance( 'Water', $waterMock );

        $sunMock = m::mock( 'Sun' );
        $sunMock->shouldReceive( 'get' )
            ->once()
            ->withNoArgs()
            ->andReturn( 50 );
        App::instance( 'Sun', $sunMock );

        // 引数なしでも注入できる
        $grape = new Grape();

        $this->assertEquals( 100, $grape->get() );
    }

}

IoCコンテナにインスタンスを登録しています。何も指定しないと、本当のクラスが実行されてしまいますからね。

この場合、Mockeryで作成したモックオブジェクトを登録します。作成済みのインスタンスを登録するためには、App::instanceを使用します。

素晴らしい。これで、テストも自由自在です。

でも、Laravelにはさらなる高みが存在しています。

コンストラクターによる注入が現在の依存注入のメインになっていました。そしてPHPでは、コンストラクターを始めメソッドの引数には、タイプヒントを指定できます。

それならば、タイプヒントが指定されていれば、その情報をもとに、IoCコンテナが自動でコンストラクターにインスタンスを渡してくれるなら、とても便利です。これが、IoCコンテナの依存の自動解決です。

なにせ、いちいちインスタンス化するための手間がかかりません。そのクラスが依存するクラスを、コンストラクターに記述するだけで済みます。他の場所に書かないため、二重に定義したり、定義が合わなくて苦労することもありません。

そして、Orangeクラスを見なおしてもらえば分かるのですが、タイプヒントを指定していないと、オブジェクトを間違って渡してしまうことができます。それは、実行するまでエラーになりません。同じメソッドを備えたオブジェクトで間違えてしまえば、不具合の発見が遅れることもあるでしょう。

では、ご覧ください。Peach.phpです。

<?php

class Peach
{
    // Laravel4標準 タイプヒントを指定した
    // コンストラクター注入システム
    // コンストラクターは依存注入専用になっている

    public function __construct( Water $water, Sun $sun )
    {
        $this->water = $water;
        $this->sun = $sun;
    }

    public function get()
    {
        return $this->water->get() + $this->sun->get();
    }

}

これが、Laravel4の標準的なコーディングスタイルになります。そのクラスで必要なインスタンスをコンストラクターのパラメーターとして指定しておき、IoCコンテナから受け取ります。

テストはOrangeTestと同じ方法で可能です。これも宿題にします。ご自身で作成してください。

この、Peachクラスを利用する、PeachCanクラスもご覧ください。

<?php

class PeachCan
{

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

    public function get()
    {
        return $this->peach * 2;
    }

}

PeachCanクラスはPeachクラスを利用するわけです。ですから、Peachクラスのタイプヒントをコンストラクターで指定しています。

インスタンス化のコードが入っていません。インスタンス化はLaravelにおまかせです。

LaravelのIoCコンテナは、このタイプヒントを利用して、再帰的に必要なインスタンスを全部揃えます。

つまり、PeachCanクラスが、(多分これも、他のクラスのコンストラクターで指定されることで)インスタンス化される時、当然Peachクラスのインスタンスが必要になります。Peachクラスをインスタンス化するには、WaterとSunクラスのインスタンスが必要です。IoCコンテナは依存を調べ、その結果、Water、Sun、Peach、PeachCanと自動的にインスタンス化し、コンストラクター経由で各クラスへ生成済インスタンスを供給します。

クラス間の依存を自動的に解決する、クラスのタイプヒントを利用した、自動依存解決システムです。Laravelが支持される理由はたくさんありますが、このコンテナ機能もその一つです。あまり、目立ちませんけどね。

さて、多分、これに関して起きる反対意見は2つあるでしょう。

一つは、依存を解決するためのクラスインスタンス以外は、コンストラクターへパラメーターとして渡せないことです。インターフェイスやクラス名以外をタイプヒントとして指定しても、タイプヒント自身を省略してパラメーターを渡しても、コンストラクターに「解決できません」と叱られます。

グッドプラクティスには流行り廃りがあります。IoCとか、DIとか言われ始める前、オブジェクト志向最初の頃のプラクティスには、コンストラクターに必要な情報を全部渡し、セッターメソッドなどでオブジェクトが保持する値を変更しないことを勧めているものがあります。オブジェクトが保持する値が、外部からやたらに変更されてしまうと、オブジェクトの振る舞いが変わりやすくなり、それによりバグが出やすくなるからです。(もちろん、テスト駆動開発より以前の概念でしょう。)

残念ながら、この古いプラクティスはLaravel4では使えません。(どうしてもというのであれば、IntegerやStringといった、ラッパークラスを用意し、それをコンストラクターで使用すれば、可能かも知れません。手間を考えると、面倒ですね。)

2つ目の反対意見は、こうした自動解決方法では、クラス間の依存が見えなくなる点でしょう。依存関係を明確に理解しやすくするために、コンストラクターの定義を利用する考え方です。

例えば、最後のPeachCanの場合、以下のように定義します。

App::bind( 'Water', 'Water' );
App::bind( 'Sun', 'Sun' );
App::bind( 'Peach', 'Peach' );
App::bind( 'PeachCan, function() {
    return new PeachCan (
        App::make( 'Peach', array(
            App::make( 'Water' ), App::make( 'Sun' )
        ) );
    );
});

App::makeの2つ目のパラメーターは、コンストラクターに渡すパラメーターを配列で指定します。

これにより、デザインパターンとか、機能の一くくりとかの依存関係をひと目で「読める」ようにできます。自動解決ができないコンテナでは、これがベストプラクティスでしょう。

ご覧の通り、自動解決を使わず、Laravelでもこの方法を取ることは可能です。できないわけでないことに留意してください。

さて、Laravelでは優秀なコンテナが自動に解決してくれます。私はもちろん、自動解決を使用することをおすすめしますが、どちらを採用するかは、好みと利点・欠点のトレードオフで決めましょう。

依存を記述する方法を取るのであれば、そうした関係が「ひと目」でわかる利点があります。その代わり、コンストラクターのパラメーター定義と、コンストラクターの定義を手動で一致させておかなくてはなりません。修正や変更が起きれば、必ずこの定義を見直す必要も起きるでしょう。

Laravel4のIoCコンテナの自動解決に、インスタンス化を任せてしまうのであれば、二重管理をする必要はなく、修正はコンストラクターのパラメーター定義とコンストラクター内でプロパティとして保存するコードだけで済みます。ただし、込み入った関係のあるクラスでは、ひと目で関係を見て取ることが、難しくなります。

理屈で説明すれば、こうなりますが、実際に使用し始めれば、自動解決は楽ですし自然であり、とてもつかいやすく、欠点を補っても余りあるものであると実感できます。

フレームワークはグッドプラクティスを楽に取り入れてくれます。どうせ、フレームワークを利用するなら、依存の解決などの機械が得意な部分は、任せてしまいましょう。「楽」にやりましょう。(ただし、必要な時に設計の手間を省かないように。;))

Laravelでのテストについて、もっと知りたい方は、Laravel Testing Decodedをどうぞ。Laravelの構造や、Laravelで大きなプログラムを作成したい方は、[Laravel: 見習いから職人へ]Laravel: 見習いから職人へをどうぞ。