【Laravel】Laravelのキモ、「サービスコンテナ」の仕組みを理解する〜後編〜

  • URLをコピーしました!

この記事ではLaravelフレームワークのキモであるサービスコンテナについて解説していきます。

この記事では後編ですので、まだ前編を読んでいない方はまずは前編を読んでください。

Twitterを眺めていたら、

「Laravelのすごいところはどこか?と聞かれてサービスコンテナと答えられない人はにわかだー!」

というツイートを見たことがあるくらい、Laravelの大きな大きな機能の1つです。

前編では以下の内容について解説しています。

  • サービスコンテナの概要
  • バインド
  • 解決

前編を読むことで「サービスコンテナって何?」という状態からは脱却できるかと思います。

後編の内容

  • DI/依存性の注入

後半のこの記事ではサービスコンテナの大きな機能であるDI/依存性の注入について深く理解していきます。

少し前編のおさらいを挟みます。

参考書籍

この記事はこれらの技術書の内容をベースにまとめています。

初学者向けではないですが、とても勉強になる内容の技術書です。

以下のリンクから購入することができます。

こちらは初心者〜実務経験6ヶ月くらいまでの方にオススメの技術書です。

対応バージョンがLaravel5.8.9と少し古いですが、1つ目の参考書より全体的に内容が易しいので青本の次のステップとしておすすめです。

ゆーたろー

この記事を読んでにわかLaravelエンジニア卒業に少しでも近づきましょう!笑

というわけで、解説していきます!

目次

[おさらい]サービスコンテナの概要

おさらいですが、サービスコンテナはLaravelフレームワークの大きく、便利な機能を1つです。

機能は大きく2つあり、

  • あるクラスのインスタンスを生成する方法を自由にカスタマイズすることができる
  • あるクラスと依存関係にあるクラスのインスタンスを管理してくれる

です。(前編にも書いています)

この記事の目玉となるDI/依存性の注入は後者の「あるクラスと依存関係にあるクラスのインスタンスを管理してくれる」に大きく関わります。

DI/依存性の注入

DI/依存性の注入とは

クラスやメソッド内で利用する機能を外部から渡す設計パターン

のことをDI/依存性の注入と言います。

DIは”Dependency Injection”の略です。

もう少し噛み砕いて説明すると

クラス(ControllerとかService)のコンストラクタやアクションメソッドに引数が定義されている場合、その引数に設定されたクラスのインスタンスを必要に応じて自動で用意してくれます。

新しく生成する場合もありますし、すでにインスタンスが存在する場合はそれを探して用意してくれたりします。

多分これはコードを見た方が早いと思うのでコードを書きます。

<?php

use Illuminate\Support\Facades\Log;
use App\Services\SampleService;

class SampleController
{
    protected $sampleService;

    // この引数の指定の仕方がまさにDI!!
    public function __construct(SampleService $sampleService)
    {
        $this->sampleService = $sampleService;
        Log::debug('サンプルクラスのコンストラクタです');
    }
}

こういうコードを見たことがありますよね。

引数のタイプヒンティング(型宣言)で特定のクラスを指定している場合。

これがまさにDIです。

Laravelを書いた人なら初心者の方でも割と馴染みのあるコードかと思います。
(と同時に、なんとなく感覚で書いている人も多いのではないでしょうか)

ここからもう少し詳しく解説していきます。

「依存関係にある」とは?

先ほどから依存という言葉を使っていますが、

そもそもプログラミングにおける依存って何??

という方もいらっしゃるかもしれません。

「依存関係にある」コードを理解するためには以下のサンプルコードをご覧ください。

<?php

class SampleController
{
    public function index()
    {
        $sampleService = new SampleService();
        $sampleService->test();
    }
}
<?php

class SampleService
{
    public function test()
    {
        // 処理
    }
}

上記コードでは、SampleControllerクラスのindexメソッドでSampleServiceクラスのインスタンスを生成して利用します。

つまり、SampleControllerクラスの動作にはSampleServiceクラスが必要です。

これを

SampleControllerクラスはSampleServiceクラスに依存している

と言います。
(SampleControllerクラスとSampleServiceクラスは依存関係にある、ということ)

依存度が高ければ高いほど密結合となります。

その依存度を下げるための設計パターンがDIです。

というわけで、依存、依存関係について理解できたと思いますのでDIの解説に戻ります。

DIは大きく以下の2つに分かれます。

  • コンストラクタインジェクション
  • メソッドインジェクション

以降は上記の2つに触れていきます。

コンストラクタインジェクション

クラスのコンストラクタの引数のタイプヒンティングで別のクラスを指定する方法

をコンストラクタインジェクションと言います。

サンプルコードを書きます。

<?php

class SampleController
{
    protected $sampleService;

    // コンストラクタインジェクション
    public function __construct(SampleService $sampleService)
    {
        $this->sampleService = $sampleService;
    }

    public function readWord(string $word)
    {
        $this->sampleService->read($word);
    }
}

$sample = app()->make(SampleController::class);
// バインドされていないので上記コードは以下でもOK
// $sample = new SampleController();
$sample->read("テストワード");

上記サンプルコードにおけるサービスコンテナの役割は以下。

  • 解決を依頼されたクラス名のコンストラクタの仮引数定義を読み取る
  • 仮引数のタイプヒンティングがクラス、インターフェースならその解決を行い、取得したインスタンスをコンストラクタの引数に渡す

仮引数のタイプヒンティングがインターフェース名の場合は、解決方法がサービスコンテナにバインドされている必要があります。(インターフェースからはインスタンスは作れませんからね)

前半の内容になりますが、インターフェースのバインド方法は以下です。

app()->bind(SampleInterface::class, function () {
    return new SampleService();
});

このようにバインドすると、SampleControllerのコンストラクタの仮引数のタイプヒンティングがSampleInterfaceインターフェースだった場合、仮引数$sampleServiceにはSampleServiceクラスのインスタンスが注入されます。

ちょっと複雑ですね。

バインドしてない状態で上記の動作をさせようとすると例外を吐きます。

なお、バインド時にインターフェースと具象クラスを対応させる場合は以下の通り省略して記述することができます。

app()->bind(SampleInterface::class, SampleService::class);

これ、よくQiitaとかで見る書き方です。

これがコンストラクタインジェクションです。

メソッドインジェクション

コンストラクタインジェクションからのこれなので大体は予想はつくと思いますが、メソッドインジェクションは

クラスのメソッドの引数のタイプヒンティングで別のクラスを指定する方法

です。

メソッドの引数に必要となるクラスのインスタンスを渡します。

同様にサンプルコードに沿って解説します。

<?php

class SampleController
{
    // メソッドインジェクション
    public function readWord(SampleService $sampleService, string $word)
    {
        $sampleService->read($word);
    }
}

$sample = app()->make(SampleController::class);
// バインドされていないので上記コードは以下でもOK
// $sample = new SampleController();
app()->call([$sample, readWord], ['word' => 'テストワード']);

メソッドインジェクションの場合はcallメソッドを使います。

  • 第一引数に実行するクラス変数とメソッド
  • 第二引数にメソッドインジェクションで注入する値以外のパラメータ

をそれぞれ渡してあげます。

上記コードではサービスコンテナの以下の働きをしてくれます。

  • callメソッドの第一引数で指定されたメソッドの仮引数定義を読み取る
  • 仮引数のタイプヒンティングがクラス、インターフェースならその解決を行い、取得したインスタンスをメソッドの引数に注入する

メソッドインジェクションの場合ももちろん、インターフェースでタイプヒンティングしている場合は予めバインドしていない場合は例外を吐きます。

少し調べてみるとcallメソッドはルーティングの書き方と同じ感じでも書けるようです。

app()->call(['SampleController@readWord', ['word' => 'テストワード']);

以上でメソッドインジェクションの解説を終わります。

[ちょっと発展]同じタイプヒンティングの引数に異なるインスタンスを注入する

ここは少し発展的な内容になるので、

もうDIについてはお腹いっぱいです!

という方は飛ばしてもらって問題なしです(笑)

タイプヒンティグにインターフェースを指定して、注入する具象クラスのインスタンスを注入することも可能です。

参考にした技術書のまんまユースケースを引用しますが、例えばユーザーの属性で通知手段を分けるケースです。

サンプルコードはもっと抽象的なコードにさせていただきますm(__)m

<?php

class FirstSampleService
{
    protected $sample;

    // コンストラクタインジェクション
    public function __construct(SampleInterface $sample)
    {
        $this->sample = $sample;
    }
}

class SecondSampleService
{
    protected $sample;

    // コンストラクタインジェクション
    public function __construct(SampleInterface $sample)
    {
        $this->sample = $sample;
    }
}

// whenメソッドでバインドする
app()->when(FirstSampleService::class)
    ->needs(SampleInterface::class)
    ->give(FogeService::class);

app()->when(SecondSampleService::class)
    ->needs(SampleInterface::class)
    ->give(HugaService::class);

上記のコードでこのような動作を表現できます。

  • FirstSampleServiceクラスのコンストラクタでは$sampleにはFogeServiceクラスのインスタンスが注入される
  • SecondSampleServiceクラスのコンストラクタでは$sampleにはHugaServiceクラスのインスタンスが注入される

DI/依存性の注入の恩恵を最大限受けるケースは?

あるクラスを具象クラスではなく抽象クラス(インターフェース)に依存させるケースです。

<?php

class SampleService
{
    // メソッドインジェクション
    public function sendMessage(SenderInterface $sender, string $word)
    {
        $sender->execute($word);
    }
}

interface SenderInterface
{
    public function execute(string $word);
}

class Mailer implements SenderInterface
{
    public function execute(string $word)
    {
        // メールでメッセージを送る処理
    }
}

class Fax implements SenderInterface
{
    public function execute(string $word)
    {
        // Faxでメッセージを送る処理
    }
}

このような実装にすることで、バインドの仕方次第でSampleServiceクラスのsendMessageメソッドの引数$senderにはSenderInterfaceインターフェースを実装したクラスのインスタンスなら何でも渡す(注入する)ことができます。

sendMessage側は引数$senderに渡されるインスタンスがどのクラスかどうかを考える必要はなく、単純にexecuteメソッドを実行しメッセージの送信処理だけを認識することができます。
(メールなのかFAXなのか、手段を考えなくてOK)

このようにあるクラスと抽象クラス(インターフェース)を依存させることでクラス間の依存関係が疎結合になる=DIの恩恵を大いに受けることができます。

とはいえ、ここまで依存関係を下げる必要があるのかどうかはそのプロジェクトによりけりとしか言いようがないので、これがDIのデファクトスタンダードというわけではないです。

以上がDI/依存性の注入の解説です。

長かったですね。お疲れ様でした。

最後に

前編、後編を通してサービスコンテナについてかなり噛み砕いて解説してきました。

サービスコンテナ、DIなどの便利な機能がデフォルトでがあるからこそ、開発者はビジネスロジックの実装に集中できるんだなと実感。

フレームワークは開発を効率化するためにあるものなのでその機能はありがたくどんどん使っていくべきだと思いますが、たまにはこんな感じで深掘りしてみるのもよりフレームワークの恩恵を感じれるので良いですね。

サービスコンテナ様様。Laravel様様。

参考記事

Laravelのオススメ教材

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

ブログを開設するなら「SWELL」が絶対オススメ!

コメント

コメントする

目次