Edited at
TECOTECDay 3

Laravelで始める依存性の注入(DI)

「依存性の注入」(Dependency injection)をご存知でしょうか。

あるクラスが依存している別の処理を、外部から渡すことで、コンポーネント間の依存度を下げるテクニックです。

Laravelにはサービスコンテナと呼ばれる依存を管理する仕組みが組み込まれています。

今回はサンプルユースケースのリファクタリングを通じて、Laravelにおけるサービスコンテナと依存性注入の仕組みを、少しだけ「わかった気」になってみましょう。


サンプルユースケース

「あるURLにアクセスすると、ダイスを2回振って出た目を合計した数値を返す」


まずは動くように作る

何も考えずに動くコードを作ってみましょう。

class DiceController extends Controller

{
public function rollDouble()
{
return mt_rand(1, 6) + mt_rand(1, 6);
}
}

すばらしい。

あとはルーティングの設定を行えば、6面ダイスを2回振った結果を返すURLの出来上がりです。

実際、シンプルな要件のシステム(開発ツール等)や、開発スピードが重視される現場であれば、

これでもいいのかもしれません。

しかし、もしTRPGのように複雑な要件が含まれるようなプロジェクトであれば、もう少し変化に強い作りにしておきたいですね。

ちょっとずつリファクタリングしていきましょう。


クラス化する

「ダイスを振っている」という動作がわかりやすくなるように、ダイスを振る部分(mt_randの処理)を、別クラスに切り出します。

class Dice

{
public function roll(): int
{
return mt_rand(1, 6);
}
}

コントローラは以下のように書き換えます。

    public function rollDouble()

{
// ダイスをインスタンス化
$dice = new Dice();

return $dice->roll() + $dice->roll();
}

名前が付いたことで説明的になり、「ダイスを2回振ってる」というのがわかりやすくなりましたね。

さらにコントローラから「6」という数字が消えました。

例えばダイスを振る処理があらゆる場所にある状態で、「ダイスを6面では無く12面にしたい」という仕様変更が来たとしても、Diceを修正するだけで良くなりました。

いい感じです。


動作をテストしたい

ところで、この「ダイスを2回振って合計した値を返す」というロジック、本当にちゃんと動いているんでしょうか。

この程度であればパッと見でも正しいことが判断できますが、実際のシステムだとすべてを目視で確認することはできそうもありません。

そこでユニットテストを書くわけですが、困ったことに、今回のロジックは簡単にテストすることができません。

毎回呼ぶたびに結果が異なるので、「ダイスを2回振って合計した値を返す」というロジックが正しく動いていることを確認しようがないのです。

(せいぜい、複数回施行して特定の確率分布に収束することを確認するぐらいでしょうか)


イカサマダイスを作る

なぜ毎回結果が違うのかというと、ランダムな値を返す普通のダイスを使っているからです。

つまり、毎回同じ値が出るイカサマダイスを使えば、テストができるようになるんじゃないでしょうか。

作ってみましょう。

class LoadedDice

{
public function roll(): int
{
return 6; // 6しか出ない!
}
}

使い方はDiceと同じですが、roll()の結果が必ず6になります。

さっそくコントローラに組み込んでみましょう。

    public function rollDouble()

{
$dice = new LoadedDice(); //イカサマダイスに置き換えた

return $dice->roll() + $dice->roll();
}

実行すると、必ず12が返ってくるようになります。

6しか出ないダイスを2回振ってるから当然ですね。


環境によって切り替える

今度は、通常の処理でも必ず12が返るようになってしまいました。

これではゲームにならないので、環境によってインスタンスを切り替えるようにしてみます。

    public function rollDouble()

{
if (App::environment('testing')) {
$dice = new LoadedDice();
} else {
$dice = new Dice();
}

return $dice->roll() + $dice->roll();
}

良さそうです。

テスト環境では12が、その他の環境では6面ダイスを2回振った結果が返ってくるようになりました。

この状態であれば、ユニットテストを書くことができますね。


インスタンス化をコントローラの外で行う

説明のために、一度コントローラを元の状態に戻します。

    public function rollDouble()

{
$dice = new Dice();

return $dice->roll() + $dice->roll();
}

これを以下のように書き換えます。

    public function rollDouble(Dice $dice) // コントローラの引数にDiceを指定

{
return $dice->roll() + $dice->roll();
}

Diceのインスタンス化の処理が消えてしまいました。

しかし、これでも同じ結果になります。

Laravelでは、ルートに紐づいたコントローラのメソッドに対して、クラス名をタイプヒントした引数を指定すると、そのクラスのインスタンスを生成し、引数として渡してくれます。

サービスコンテナが担う機能の一つで、依存性の注入の手助けをしてくれます。

コントローラが依存しているDiceのインスタンスを、外部から注入(インジェクション)しているわけです。

メソッドだけでなく、コンストラクタでインジェクションすることもできます。

class DiceController extends Controller

{
private $dice

public function __construct(Dice $dice) // ←インジェクション!
{
//プロパティとして持っておく
$this->dice = $dice;
}

public function rollDouble()
{
return $this->dice->roll() + $this->dice->roll();
}
}

これをコンストラクタインジェクションと呼びます。

対して、メソッドでインジェクションする方はメソッドインジェクションと呼びます。

コマンドやミドルウェアなど、Laravelのスキームに組み込まれたクラスはだいたいコンストラクタでインジェクションできます。

覚えておきましょう。


共通部分を切り出す

DiceLoadedDiceは、返す値は違いますが、roll()というメソッドで整数値を返すという大枠のつくりは同じです。

次のリファクタリングに向けた準備として、まずこれをインターフェースとして切り出しておきます。

// 「振れるダイス」インターフェース

interface RollableDice
{
public function roll(): int; // 「引数無しのrollメソッドが整数値を返す」と定義
}

// 普通のダイス

class Dice implements RollableDice
{
public function roll(): int
{
return mt_rand(1, 6);
}
}

// イカサマダイス

class LoadedDice implements RollableDice
{
public function roll(): int
{
return 6;
}
}


結合設定

サービスコンテナがインスタンスをインジェクションする際のロジックは、自分で定義しなおすこともできます。

これを結合といいます。

結合の設定はServiceProviderの中で行います。

AppServiceProviderでもいいし、数が増えてきたら独自のServiceProviderを追加してもいいと思います。

今回はAppServiceProviderに設定します。


\App\Providers\AppServiceProvider

    public function register()

{
//第一引数:結合するクラス名
//第二引数:インスタンスを返すコールバック関数
$this->app->bind(Dice::class, function ($app) {
return new Dice();
});
}

こう書くと「シンプルな結合」となり、インジェクションする度に別のインスタンス(第一引数に指定されたクラスのインスタンス)が作られます。

結合の設定をしない場合も同じように動作します。

対して、以下のように書くと、インスタンスをシングルトンとして生成してくれるようになります。

最初に呼ばれたときにインスタンスが1つ作られ、以降はそれが使いまわされるようになります。


\App\Providers\AppServiceProvider

    public function register()

{
// bindではなくsingletonメソッドを使う
$this->app->singleton(Dice::class, function ($app) {
return new Dice();
});
}


インターフェースに依存させる

インターフェースや抽象クラスを結合のキーにすることもできます。

先ほど作ったRollableDiceに対し、Diceを結合してみましょう。


\App\Providers\AppServiceProvider

    public function register()

{
//結合するクラス名をインターフェースにし、Diceのインスタンスを返す
$this->app->singleton(RollableDice::class, function ($app) {
return new Dice();
});
}

そして、コントローラのタイプヒントをRollableDiceに変更します。

    public function rollDouble(RollableDice $dice) // ←タイプヒントをDiceからRollableDiceに変更

{
return $dice->roll() + $dice->roll();
}

実行してみると、タイプヒントはインターフェースですが、インジェクションされるのはDiceなので、正しく動作します。1

キーからインスタンスを生成する動作を、依存関係を解決すると言ったりします。

テスト用の切り替え処理は、結合処理の中に書きます。


\App\Providers\AppServiceProvider

    public function register()

{
$this->app->singleton(RollableDice::class, function ($app) {
if (App::environment('testing')) {
return new LoadedDice();
}
return new Dice();
});
}

テスト環境ではLoadedDiceがインジェクションされ、その他の環境ではDiceがインジェクションされるようになります。


依存度を確認

コントローラ内でインスタンス化していたころのコードと見比べてみましょう。



    public function rollDouble()

{
if (App::environment('testing')) {
$dice = new LoadedDice();
} else {
$dice = new Dice();
}

return $dice->roll() + $dice->roll();
}




    public function rollDouble(RollableDice $dice)

{
return $dice->roll() + $dice->roll();
}

古いやり方では、コントローラはDiceLoadedDiceの両方の生成処理を知っていなければなりませんでした。

ダイスの種類が増えたり、コンストラクタに引数が必要になった場合、都度この処理を書き換えなければなりません。

これらの2つのクラスに依存しているということです。

このメソッドの役割は「ダイスを2回振って合計を返す」だけなのに、余計なことを知りすぎています。

一方新しい方は、RollableDiceというインターフェースのみに依存するようになりました。

DiceLoadedDiceを生成するのにどういう手順が必要か、知る必要がなくなったわけです。

RollableDiceインターフェースは、これを実装しているクラスは「振れるダイス」であるということを保証します。

ダイスを2回振って結果を返すだけなら、「振れるダイス」ということだけ保証できていればそれで十分なのです。


インジェクションの種類

コントローラのメソッドやコンストラクタでインジェクションする以外にも、インスタンスを取得する方法は用意されています。

Illuminate\Foundation\Applicationのインスタンス2にアクセスできる場所であれば、make()メソッドが使えます。

$dice = $this->app->make(RollableDice::class);

Illuminate\Foundation\Application にアクセスできなくても、

resolve()ヘルパを利用すれば、気軽に依存解決することもできます。

$dice = resolve(RollableDice::class);

ただし、本当にあらゆる場所から呼べてしまうので、

やりすぎると影響範囲が膨大となり、後々苦しむことになりかねません。

慎重に使うようにしましょう。


多重インジェクション

インジェクションされるクラスでも、また別のクラスの処理に依存している場合、

インジェクションを多重で行うことが起こり得ます。

例えば、もし乱数生成にmt_randではなく、別のロジック3を使いたい場合、どうすれば良いでしょうか。

コントローラと同じように、自作のクラスにもインスタンスをインジェクションすることができます。

// DiceをMarvelousRandToolクラスに依存させる

// ※MarvelousRandToolの実装は省略
class Dice implements RollableDice
{
private $randTool;

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

public function roll(): int
{
return $this->randTool->rand(1, 12);
}
}

サービスプロバイダの設定を省略している場合、Diceの依存を解決すると、

自動的にMarvelousRandToolも依存解決されて、必要なインスタンスがインジェクションされます。

設定している場合、コンストラクタに渡すインスタンスの定義が必要です。


\App\Providers\AppServiceProvider

    public function register()

{
$this->app->singleton(RollableDice::class, function ($app) {
if (config('app.env' === 'testing')) {
return new LoadedDice();
}
// 乱数生成機もサービスコンテナから取得する
return new Dice($app->make(MarvelousRandTool::class));
});
}

MarvelousRandToolのインスタンスも、サービスコンテナを使って依存解決していることに注目しましょう。

結合設定は別の場所で行うことで、ここでもお互いの依存度を下げることができます。


終わりに

ほんのさわり程度でしたが、サービスコンテナの機能とDIの仕組みの説明でした。

DI自体はLaravelに関係なく、いろんなシステムで採用されているパターンですが、Laravelのサービスコンテナを利用するとDIが便利に使えることが分かっていただけたでしょうか。

今回の記事のように少しずつリファクタリングしながら徐々に導入すると、利点や特徴もわかってくると思います。

皆さんもぜひ使ってみてください。





  1. ちなみに、結合処理を書かないと、インターフェースをインスタンス化しようとしてエラーになります。 



  2. 必ず1つ生成されているアプリケーションクラスです 



  3. 例えば既に決まった乱数テーブルから取得するとか