Laravel4、コンストラクターによる依存性の注入

タグ: Laravel4   テスト志向   依存性注入  

Lravel4のIoCコンテナはコントラクターによる注入ができるようになりました。

なにも単体テストが必要無いような小さなアプリやツールに、必ず使えという意味ではありません。簡単なアプリにこのような複雑な仕組みを使用したのでは、かえって面倒です。

これはきちんとした単体テストの実行が求められるほど大きなアプリ規模の開発で便利な機能です。

当然ながら、フレームワークで用意されている機能を使用するか、しないかは、開発者の方が決めることです。Laravelが強要することはありません。

もし、テスト可能なコードを書こうと思うのでしたら、LaravelのIoCコンテナを便利に使用できます。

具体的に小さなコードをご覧ください。コントローラーでDBアクセスする場合です。

class AppleController extends BaseController
{
    public function getAll()
    {
        return Apple::get()->toArray();
    }
}

AppleがEloquent ORMのモデルです。これによりapplesテーブルの全レコードが取得できます。get()からのリターン値は、Eloquentモデルの配列で、toArray()、toJson()が用意されており、簡単に結果を変換できます。

しかし、このコードはデータベースとコントローラーが直接結びついています。ルーティングのテストを行うためにデータベースの準備が必要になります。ここでは簡単なロジックですが、複数のテーブルを使用したり、リレーションが張ってあれば、単体テストというには、実に複雑な準備が必要になります。

そこで、工夫しましょう。

class AppleController extends BaseController
{
    protected $db;

    public __construct(Apple $db)
    {
        $this->db = $db;
    }

    public function getAll()
    {
        return toArray($db->get());
    }
}

Laravelはコンストラクターの引数に指定されたクラスヒントのAppleをみて、自動的にAppleクラスのインスタンスを生成し、$dbに渡してくれます。

テストのためにスタブを挿入する場合は、明示的にAppleクラスにスタブのクラスを割り当てるように、事前に設定しておきます。

app::bind('Apple', 'AppleStubClass');

IoCで明示的にコンテナ名称Appleに割りつけると、それはLaravelによる自動的なコンストラクターへの注入より、優先されるため、スタブを動作させることができます。Laravelはクラス名らしきものがあれば、まずIoCへ登録された名前ではないかと調べ、登録済みであれば登録内容に従い、クラスのインスタンスを生成し、未登録ならば、通常のクラスとみなし、この場合は自動的にインスタンスを生成します。

これでも良いのですが、コードを読んでAppleが交換可能であるということが、読み取れません。それを明示的に行いましょう。

applesテーブルに対する操作は直接他の場所に書かずに、まとめます。

まず、インターフェイスを作成します。名前の最後をInterfaceとしているのはLaravelのコーディング規則に従ったものですが、コアの修正などに関係しない限り、特別な規約はありません。

interface AppleRepoInterface {}

インターフェイスですから、本来はサポートする関数名を全部記述したほうが良いでしょう。しかし、全部記述してしまうと、たった一つの関数のスタブとして働かせる必要がある場合でも、全部の関数の宣言を記述する必要が起きてしまいます。(それが、インターフェイスの役割ですから、当然ですね。)

実際に書くか、書かないかは自分で決めてください。

直接Eloquentを呼び出さずに、このインターフェイスを実現したクラスを一枚噛ませます。

class AppleEloquentRepo impliments AppleRepoInterface
{
    function getAllRecode()
    {
        return Apple::get()->toArray();
    }
}

テスト用のスタブは簡単に書けます。簡単に書くため、配列に変換しています。配列のAppleクラスのままでは、テスト用のスタブを書くのが面倒になるためです。でも、面倒だと思わない方なら、いちいちリターンを配列に変換する必要はありませんよ。

class AppleStubRepo impliments AppleRepoInterface
{
    function getAllRecode()
    {
        retunr array(
            array('id' => '1', 'name'=>'ふじ', 'color'=>'赤'),
            array('id' => '2', 'name'=>'ジョナゴールド', 'color'=>'黄色'),
        );
    }
}

これらのクラスを呼び出すコントローラー側では、クラスヒントにインターフェイス名を使います。

class AppleController extends BaseController
{
    protected $repo;

    public __construct(AppleRepoInterface $repo)
    {
        $this->repo = $repo;
    }

    public function getAll()
    {
        return $db->getAllRecode();
    }
}

さてこの場合、コンストラクターの引数はインターフェイスです。実際のクラスではないため、インスタンスを生成することができず、Laravelは自動的に解決することもできません。

そのため、明示的にAppleRepoInterfaceがどのクラスを実際に示すのか、事前に宣言しておく必要があります。

app::bind('AppleRepoInterface', 'AppleEloquentRepo');

テストの場合はスタブに切り替えてあげれば良いわけです。

app::bind('AppleRepoInterface','AppleStubRepo');

これでコントローラーとストレージ(リポジトリ)を分離できました。コントローラの単体テストはテーブルに縛られずに実行できます。

そして、AppleEloquentRepoクラスのテストも、テーブルアクセスと関係のないロジックを考慮する必要無くなり、DBテーブルとのやり取りに集中できますので、単純になります。

この例は単純にするため、依存するクラスを一つだけにしましたが、もちろん複数のクラスに依存していても、同様に処理できます。また、クラス間の依存が階層的にネストしている場合でも、各クラスで同じようにコーディングでき、それぞれが分離され、テストしやすく、交換もしやすくなります。

なお、今回のコードは直接タイプしたもので動作確認を行なっていません。ご了承ください。