Laravel、コンテナによる依存解決とは

タグ: Laravel5.1LTS   Laravel  

これは、Laravel Advent Calendar 2015の2015年12月23日公開分です。

小ネタです。Laravelのドキュメントでよく見かける「コンテナによる依存解決」とは、何でしょうか。初めて公式ドキュメントを読む人には、意味不明です。そのための解説です。

コンテナとは

Laravelのコンテナの正式名称はサービスコンテナです。名前自体は何度か変更されているので、余り厳密に名称だと捉えないほうがよいでしょう。(呼び方についてもいろいろと世間にはうるさい人がいるので、結局Symfonyと同じにしたというところでしょうか。)

ある文字列のキーでインスタンスやインスタンスの生成方式を登録して、呼びだされた時にインスタンスを返してあげる役割のクラスです。一番シンプルな説明です。もっと厳密にはDIコンテナとかサービスローケータとか、IoCコンテナとかいう名称で情報をあたってください。おっと、厳密に知識を追いかけるのであれば、PHP界だけの定義に留まってはいけません。もっと大きなカテゴリーで理解してください。

厳密な知識より、簡単な説明を好む方は、続きをどうぞ。

依存解決とは

必要なものを取得することです。依存取得と行ったほうが簡単ですが、必要なものがまた別のものを必要としている場合、それも取得します。そのものが、また別のものを…もちろん、「再帰的」に必要なものを全部取得します。ですから、単純な依存取得と言わずに、依存解決とちょっともったいぶる言い方をするのでしょう。元は英語で、解決の部分はresolveと言う動詞です。

みなさんもうおなじみのComposerでは、AパッケージがBパッケージを必要とし、BパッケージがCパッケージを必要としている時に、Aパッケージだけを"require"すれば、BもCも取得できます。これもパッケージの「依存解決」です。

PHPでは、必要とする/される単位は基本的にクラスです。ちょっとだけおさらいです。

class A {
    function __construct()
    {
        $this->b = new B();
    }
}

class B {
    function __construct()
    {
        $this->c = new C();
    }
}

class C {
}

$a = new A();

とてもシンプルな実装です。シンプルな機能をテストを行わずに、作り捨てする場合、この方法がベストです。絶対的に良い悪い方法があるのではなく、状況により「より適している」方法が存在するだけです。

ここには有益なことを行うコードが入っていませんが、いろいろ機能を入れ込み「Aで必要なのがBではなくて、別のクラスだ」という要求があれば、Aクラスに手を入れる必要があります。また、AクラスでBクラスと絡むコードが存在する場合にテストしようとすると、Bクラスの動作にAクラスは影響を受けてしまいます。もしかしたら、Aクラスをテストしている間に、Bクラスに手を入れられて、バグってしまうかもしれません。BクラスがAクラスに影響を与えると、Aクラス自身の動作のチェック(ユニットテスト)を行おうとしているのに、Bクラスの動作に振り回されることになります。これではAとBを絡めた結合テスト(機能テスト)しか行えません。(往々にして、こんな状況では「Bは来週末にできるよ」なんて言われて、来週になると「もうちょっと先」になります。ずっと延びると、「じゃあ、ここは実機で確認…」という笑えないジョークのようになることもあります。)

そこで、「自クラスの中で必要なクラスのインスタンスを生成せずに、外部で生成したインスタンスを渡してもらえば、必要に応じてインスタンスを交換しやすくなるよね。ユニットテストの時はダミーとかテスト用のクラスを渡して、自分のクラスの機能だけをチェックできるよね。」っという考えができたわけです。外部から依存インスタンスを渡すため、依存(性)の注入と呼びます。

英語は「依存/依存状態/依存性」なのですが、最初に訳した人が「依存性の注入」と訳しちゃったんでしょうね。わけのわからない日本語です。日本語で「依存性の注入」と呼んでも「なんのこっちゃ」ですから、最近は「依存の注入」とか「依存注入」と訳されることが多くなりました。

外部から渡せば良いだけなので、渡し方にはいろいろありますが、最もよく目にするのはコンストラクターを使用し、依存するクラスインスタンスを渡す方法です。

class A {
    function __construct($b)
    {
        $this->b = $b;
    }
}

class B {
    function __construct($c)
    {
        $this->c = $c;
    }
}

class C {
}

// クラスの「外部」でインスタンス化してます
$a = new A(new B(new C()));

// Bの依存クラスをDに変更だ
$a = new A(new B(new D()));
// AとBのコードには手を入れる必要ない!

// Aのユニットテストをする
$a = new A(new StubB());
// Bの代替クラスやダミーを渡し、Aだけをテストできる!!

コンテナを使わないインスタンス取得

話を戻します。「コンテナによる依存解決」とは何かを理解しようとしています。「依存解決」とその元となる概念の「依存注入」について、たった今解説しました。後は「コンテナによる」の部分です。

逆に「コンテナによらない」場合を考えましょう。つまり、「コンテナを使わない」です。

すると、直前の「依存解決とは」節で例示した2つのサンプルコードでは、全く「コンテナ」が出てきていません。ええ、「コンテナ」使っていません。

コンテナを使う依存解決

Laravelのコンテナの特徴は、コンストラクターを通して依存が指定してあれば、再帰的に解決してくれることです。

では、「コンストラクターを通して依存を指定する」とは、何のことでしょう。

class A {
    function __construct(B $b)
    {
        $this->b = $b;
    }
}

Aクラスのみ書きなおしてみました。違いはタイプヒンティングです。PHPの構文により、$bに渡されるインスタンスのクラスがBであると、タイプヒントで指定しました。

同様にBクラスも修正した後に、PHPのnew文を使用する代わりに、Laravelのコンテナを利用してクラスAのインスタンスを取得します。

// コンテナへのアクセス方法はいろいろあるが、ファサードを使う場合。
$a = App::make('A');

キーとして文字列のAを渡しています。キーAに対するインスタンス化の方法やインスンタンスは登録されていません。そこで、Laravelのコンテナは、'A'がクラス名と想定し、クラスAを自動的にnewしてインスタンスを取得してくれます。

その際、Aのコンストラクターには「Bが必要だ」とタイプヒントで指定されています。コンテナはBクラスも同様に「依存解決」します。Bのコンストラクターには「Cが必要だ」と指定されています。そこで、クラスCのインスタンスも「コンストラクターにより依存解決」されます。

つまり、Laravelのコンテナはインスタンスの取得方法が登録されていれば、その方法でインスタンスを生成します。未登録の場合は、それを「具象クラス」としてインスタンス化できるか試します。既に例示したように、解決対象が'A'と指定されていて、A具象クラスが存在していれば、それをインスタンス化してくれます。

わざわざA具象クラスと書いたのは、Aが抽象クラスやインターフェイスの場合は、自動でインスタンス化できないからです。もちろん、その名前でインスタンス化の方法やインスタンス自身が登録されている場合は別です。

ここまでで、3つのポイントを理解できたら万々歳です。

  1. コンテナはクラスのインスタンス化方法を「キー」により登録しておくクラスである。
  2. キーが具象クラスの場合、インスタンス化方法(解決方法)が登録されていなくても、自動的にnewしてインスタンス化してくれる。
  3. コンテナによりインスタンス化されるクラスのコンストラクターに、タイプヒントにより引数として受け取るインスタンスのクラスが指定してあれば、それをキーとして解決を試みる。

ドキュメント

これで公式ドキュメントに「コンテナにより依存解決されます」という魔法の言葉が書かれていても、煙に巻かれる感覚はなくなるでしょう。

「コンテナによる依存解決」と書かれている近くには、コンストラクタやメソッドの引数で、タイプヒントにより依存クラスを指定し、インスタンスを受け取っているサンプルコードを目にすることでしょう。(コントローラーのアクションメソッドなど、特定のメソッドでもタイプヒントにより依存解決ができます。全部クラスの全メソッドではありません。そんなことをしたらオーバーヘッドが大きすぎて、フレームワークの動作がより重くなってしまいます。)

そうしたら、何も難しいことを考えずに、「ああ、ここではタイプヒントでクラスを指定すれば、自動的にインスタンスが渡されるんだ」と思ってください。便利に使ってください。

忘れてはならないこと

おさらいになります。

コンテナにより依存解決されるためには、タイプヒントでクラスを指定する必要があります。

依存解決されるクラスが、更に依存をコンストラクターのタイプヒントで指定してあれば、それらも自動で解決されます。

更に応用するなら

ユニットテストであるインスタンスをテスト用のインスタンス(モックやダミー)に変更したい場合は、そのクラス名でコンテナにインスタンス化方法を指定するか、生成済みのインスタンスを登録してください。既に説明したように、キーに指定した名前による自動的な具象クラスのインスタンス化より優先して、コンテナは登録されているインスタンス化方法やインスタンス自身を返します。これにより、インスタンスが置き換えられるわけです。

あるクラスを別のクラスインスタンスへ「交換」する場合でも、上記のようにクラス名を使ってインスタンス化方法やクラスを登録できます。しかし、明らかにインスタンスを交換できるようにする場合は、事前にクラスの代わりにインターフェイスを使用するのが、「きれい」なやり方です。

インターフェイスは自動的には解決できませんので、事前にインスタンス化方法をインターフェイス名で登録しておきます。そのインスタンス化方法を変更することで、外部からインスタンスを交換します。