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

Tags : Laravel4  

さて、いろいろな果物のクラスを作ってきました。

せっかくですから、ちょっと商売を初めましょう。生ジューススタンドです。果汁100%、フレッシュジュースです。

只今、夏の真っ盛り。桃が旬です。

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

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

これで良し。簡単ですね。

ところが、季節により、商品が入れ替わります。夏も終わりました。桃は旬が短いです。まだ、旬が続いているぶどうに切り替えたいとします。(えー、聞いてないなあーという不満が良く沸き上がったりします。予定されていた変更なんですが… :D)

するとPeachクラスをGrapeクラスへ書き換えなくてはなりません。しかも今回だけでありません。果物の旬は次々入れ替わり、そのたびごとに書き換える必要が起きます。これはちょっと嫌ですね。

一つの手は、コードを直接変更せず、IoCコンテナマジックでインスタンスを無理やり変えてしまうことです。

App:bind( 'Peach', 'Grape' );

できちゃうんです。

しかし、あまり美しくありません。この手法でできることはできるんですが、Peachという結合名/抽象名に結びつけた実際のクラス/具象名に、getメソッドが備えられているか、はっきりしません。

もちろん、今書いたばかりの私達はGrapeに存在することは分かります。ですが、まだ見ぬマンゴークラスやドラゴンフルーツクラスに、存在しているか分かりません。

そこで、この様な「交換可能」にしたい場合や、クラス間のやり取りを明確にしたい場合は、インターフェイスの出番です。

インターフェイスはクラスに存在することを期待するメソッドを定義したものです。

<?php

interface FruitInterface
{

    public function get();

}

これを果物のクラス全部に適用します。この例はPeachクラスですが、それ以外のクラスも同じように、Laravel標準パターン(コンストラクタにタイプヒントで依存するクラスを記述し、インスタンスを取得する)に書き直してくださいね。

<?php

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

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

}

ぶっちゃけ、PHPだけ使用するなら、何が良いのか分かりません。もし、これがコンパイル言語でしたら、実行前のコンパイルの段階で、エラーが発生するので、指定されたメソッドがないとか分かりますから、メリットも理解できるでしょう。(もしくは、プログラムのデザインパターンや、いろいろな原則を学ぶ必要があります。)

ところが、エディターやIDEは進化しており、だいぶ昔から、インターフェイスに「必要だ」と定義したメソッドが足りないとか、ちゃんと教えてくれます。(通常PHP対応IDEなら、標準的な機能です。軽量エディターであると、備わっていないかもしれません。プラグインなどで利用できるかも知れません。)

つまり、implements FruitInterfaceと書いた時点で、getメソッドがなければ教えてくれます。IDEやエディターが「無いクラスを実装する?」と尋ねてきて、お願いすれば、空のメソッドを用意してくれるなんて、もう標準的な機能です。

ですから、コーディングの時点で、もうgetを用意するように強要してくれます。このおかげで、実装し忘れることも普通はありません。

全ての果物クラスにFruitIntefaceを適用したら、Juiceクラスを書き直しましょう。

<?php

class Juice
{

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

    public function make()
    {
        return $this->fruit->get() * 20;
    }

}

ハイ終了。めでたし、めでたし。これを利用するクラスがあればすぐに実行して…エラーになります。

なぜなら、いくらLaravelのIoCコンテナの自動解決が便利でも、インターフェイスは「必要なクラス」を定義したお約束、契約でしかありません。つまり、実行コードがありません。ですから、インターフェイスをインスタンス化できません。インスタンス化できないものをインスタンス化しようとしても無理です。

そこで、IoCコンテナにFruiteInterfaceをインスタンス化する時に使用する、クラスを指定する必要があります。

App::bind( 'FruitInterface', 'Peach` );

これじゃ、PeachGrapeを結びつけた時とおんなじですって?

その通りですね。コードは同じです。

しかし、インターフェースは普通サフィックスにInterfaceを付けることが多いですから、この一行を読んだ人には、「FruitInterfaceは交換可能であり、Peachクラスがインスタンス化される。」とわかるのです。意図が明確で、読み取れます。

さて、テストです。PeachTestはそのままで、Peachクラスをテストできます。他の果物をLaravel標準、コンストラクターにタイプヒント指定して、インスタンスを自動に受け取るスタイルに書きなおしたのであれば、同じようにテストしてください。

ちょっと頭をひねるのは、Juiceクラスの方です。インターフェイスをコンストラクターで受け取るコーディングの場合、どうすればいいのでしょう?

JuiceTest.phpを書いてみましょう。

<?php

use Mockery as m;

class JuiceTest extends TestCase
{

    public function testGet()
    {
        $fruitMock = m::mock( 'FruitInterface' );
        $fruitMock->shouldReceive( 'get' )
            ->once()
            ->withNoArgs()
            ->andReturn( 3 );

        $juice = new Juice($fruitMock);

        $this->assertEquals( 60, $juice->make() );
    }

}

インターフェイス名そのままのモックを作ります。難しくありません。

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