1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DIがようやく腑に落ちたので、Laravelサンプルコードと具体例を交えて解説する

Posted at

はじめに

プログラミングには「DI」という概念があります。Dependency Injection、「依存性注入」なんて言ったりするのですが、果たしてこれを初見で理解した方はいるのでしょうか?
そんなレベルで、このDIは難解です。メリットがよく分からない、という方も非常に多いと思います。

かくいう自分もそうでした。「『単体テストしやすい』ってなんやねん!!」といつもなってました。
これまで触れてきたLaravel、.NET Coreなど、近頃は多くのフレームワークで、だいたいDIの概念はあるのですが、どっちについても「???」となってました。
しかし、色々と経験を積むうちに、ようやくそのDIについて、メリットを理解できるまでになりました。今では「これは便利やなー」と思えるぐらいになってます。

そんな訳で今回は、そのDIのメリットについて語っていきます。
なるべく難しい単語は使わず、「なるほど!」と思ってもらえるように頑張ります!
※ちなみに、この記事内での言語は、PHP、Laravelを使用しています。
※サンプルコードはこちら

具体的なケース

連携できるのは2ヶ月後!?

以下のようなサービスを開発するとします。

  • ポケモンずかんを表示するWebシステムを作りたい
  • ポケモンの一覧、詳細表示を行う
  • 上記の画面や機能をそれぞれ作る
  • ただし、実際にAPI連携可能になるのは、社内監査の都合で、今から2ヶ月後

※ポケモン一覧は、こちらのAPIを使用して表示します
https://pokeapi.co/

このようなことは、実際の開発現場だとよくあります。

  • 契約や課金が必要で、可能な限りギリギリまで課金したくない
  • APIの調達に時間がかかる
  • API側も開発が必要なプロジェクトで、APIの開発完了がだいぶ先

さあ困りました。今から2ヶ月間は、APIを実行することができません。
2ヶ月間何も出来ないのか…。パット見そのように考えてしまいますが、もう少し考えてみましょう。
本当に何もできないのか?というと、そういう訳ではないはずです。

実際のAPIを利用できない期間であっても、以下のような作業は進めることが可能です。

  • クライアント側のデザイン作成
  • 一覧や詳細画面のUI設計
  • コントローラーやモデルの実装

APIを実行して、実際にポケモン一覧を取得することは出来ないですが、その部分はモック(ダミーデータを返却する仕組み)でも作れるはずです。
それ以外の画面デザインや画面制御の部分は、この時点でも作れるはずですね。

ということで、開発者は開発を進めることにしました。具体的には、以下のような流れです。

  • 「ポケモン一覧を表示する画面・デザイン」といった、APIと直接関係ない箇所の開発は進める
  • 要となる「REST APIを使用したポケモン一覧取得」は、モックを作成する。実際にはAPIを使用せず、固定のポケモンインスタンスの一覧を返却するようにする

それでは、実際に作ってみましょう。以下、サンプルコードです。

ポケモンクラスの作成

app/Models/Pokemon.php

<?php
namespace App\Models;

class Pokemon
{
    public $id;
    public $name;

    public function __construct($id, $name)
    {
        $this->id = $id;
        $this->name = $name;
    }
}

モックリポジトリの作成

app/Repositories/PokemonMockRepository.php

<?php
namespace App\Repositories
;

use App\Models\Pokemon;

class PokemonMockRepository
{
    private $pokemons = [];

    public function __construct()
    {
        $this->pokemons = [
            new Pokemon(1, 'フシギダネ'),
            new Pokemon(2, 'フシギソウ'),
            new Pokemon(3, 'フシギバナ'),
            new Pokemon(4, 'ヒトカゲ'),
            new Pokemon(5, 'リザード'),
            new Pokemon(6, 'リザードン'),

        ];
    }

    public function all()
    {
        return $this->pokemons;
    }

    public function find($id)
    {
        return collect($this->pokemons)->firstWhere('id', $id);
    }
}

コントローラーの作成

app/Http/Controllers/PokemonController.php

<?php	
namespace App\Http\Controllers;

use App\Repositories\PokemonMockRepository;

class PokemonController extends Controller
{
    private $repository;

    public function __construct()
    {
        $this->repository = new PokemonMockRepository();
    }

    public function index()
    {
        $pokemons = $this->repository->all();
        return view('pokemons.index', compact('pokemons'));
    }

    public function show($id)
    {
        $pokemon = $this->repository->find($id);
        return view('pokemons.show', compact('pokemon'));
    }
}

ルーティングの作成

route/web.phpに以下を追加する

Route::get('/pokemons', [PokemonController::class, 'index'])->name('pokemons.index');
Route::get('/pokemons/{id}', [PokemonController::class, 'show'])->name('pokemons.show');

ビューの作成

resources/views/pokemons/index.blade.php

// ※一部のみ記載
<h1>ポケモン図鑑</h1>
<ul>
    @foreach($pokemons as $pokemon)
        <li>
            <a href="{{ route('pokemons.show', $pokemon->id) }}">
                {{ $pokemon->name }}
            </a>
        </li>
    @endforeach
</ul>

resources/views/pokemons/show.blade.php

// ※一部のみ記載
<h1>{{ $pokemon->name }}の詳細</h1>
<p>ID: {{ $pokemon->id }}</p>
<p>名前: {{ $pokemon->name }}</p>
<a href="{{ route('pokemons.index') }}">戻る</a>

上記のようなコードとなります。これで、ポケモンの一覧を表示できます。
以下のような画面ですね。(びっくりするぐらいシンプルな画面ですが・・・!笑)

pokemon1.png
pokemon2.png

モックと実APIの切り替え

2ヶ月後、APIの連携が可能になった時点で、モックから実際のAPIに切り替える方法は非常に簡単です。
以下のような手順で実装できます。

  1. PokemonApiRepository クラスを実装
  2. コントローラーでリポジトリを変更

PokemonApiRepositoryの実装

app/Repositories/PokemonApiRepository.php

<?php

namespace App\Repositories;

use App\Models\Pokemon;
use Illuminate\Support\Facades\Http;

class PokemonApiRepository
{
    private $baseUrl = 'https://pokeapi.co/api/v2/';

    public function all()
    {
        $response = Http::get($this->baseUrl . 'pokemon?limit=20');
        $data = $response->json()['results'];

        return collect($data)->map(function ($item, $index) {
            $id = $index + 1;
            return new Pokemon($id, $item['name']);
        })->all();
    }

    public function find($id)
    {
        $response = Http::get($this->baseUrl . "pokemon/{$id}");
        $data = $response->json();

        return new Pokemon($data['id'], $data['name']);
    }
}

コントローラーの修正

app/Http/Controllers/PokemonController.php

<?php
namespace App\Http\Controllers;

use App\Repositories\PokemonApiRepository;
// use App\Repositories\PokemonMockRepository; // コメントアウト

class PokemonController extends Controller
{
    private $repository;

    public function __construct()
    {
        // ※重要
        // $this->repository = new PokemonMockRepository(); // コメントアウト
        $this->repository = new PokemonApiRepository(); // 新しいリポジトリを使用
    }

    public function index()
    {
        $pokemons = $this->repository->all();
        return view('pokemons.index', compact('pokemons'));
    }

    public function show($id)
    {
        $pokemon = $this->repository->find($id);
        if ($pokemon) {
            return view('pokemons.show', compact('pokemon'));
        }
        return abort(404);
    }
}

これで、最初はMockを使用した開発を行い、途中で実際のAPIに切り替える事ができます。

DIは外部IFを使用した開発に便利

さて、ここまででも十分開発は可能ですが、このやり方だと以下のような問題点があります。

  • 上記コントローラーで「※重要」と書いた箇所。Mockと実APIで、わざわざリポジトリを切り替えないといけない
  • 1クラスぐらいであればまだ良いが、複数クラスでMockと実APIを切り替える場合、面倒だし、切り替え忘れなんてことも発生する
  • うっかり戻し忘れ、本番環境でMockを使用した画面になってしまう・・・なんてことも

これは非常に問題ですね。どうすればいいでしょうか?
そう、これを解決するのがDIなんです。

具体的なコードを見た方が早いですね。
まずは、インタフェースを作成します。

app/Repositories/PokemonRepositoryInterface.php

<?php

namespace App\Repositories;

interface PokemonRepositoryInterface
{
    public function all();
    public function find($id);
}

このインタフェースは、「ポケモンのデータを取得するサービスは、必ず『一覧取得』『詳細取得』の機能を用意してくださいね!」ということを表しています。

次に、これまで作ったMock・実APIのリポジトリに対し、上記のインタフェースを実装するようにします。
※必要な部分のみ記載しています

app/Repositories/PokemonMockRepository.php

<?php
namespace App\Repositories;

// 以下のように実装
class PokemonMockRepository implements PokemonRepositoryInterface
{
    // その後は省略
}

app/Repositories/PokemonApiRepository.php

<?php
namespace App\Repositories;

// 以下のように実装
class PokemonApiRepository implements PokemonRepositoryInterface
{
    // その後は省略
}

次に、既存のAppServiceProviderに、以下のコードを追加してください。(サービスプロバイダを新規作成することもできますが、ここでは簡略化します)

app/Providers/AppServiceProvider.php

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
// 以下、追加
use App\Repositories\PokemonRepositoryInterface;
use App\Repositories\PokemonMockRepository;
use App\Repositories\PokemonApiRepository;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        // 以下、追加
        $this->app->bind(PokemonRepositoryInterface::class, function ($app) {
            return config('app.use_mock_pokemon_api')
                ? new PokemonMockRepository()
                : new PokemonApiRepository();
        });
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

また、環境変数とconfigを設定します。

// .envに以下を追加
USE_MOCK_POKEMON_API=true

// config/app.php
'use_mock_pokemon_api' => env('USE_MOCK_POKEMON_API', false),

最後に、コントローラーを以下に修正します。ここがもっとも肝となる箇所になります。

<?php
// コントローラー
namespace App\Http\Controllers;

use App\Repositories\PokemonApiRepository;

class PokemonController extends Controller
{
    // 重要!!!!
    public function __construct(protected PokemonRepositoryInterface $repository)
    {
    }

    public function index()
    {
        $pokemons = $this->repository->all();
        return view('pokemons.index', compact('pokemons'));
    }

    public function show($id)
    {
        $pokemon = $this->repository->find($id);
        if ($pokemon) {
            return view('pokemons.show', compact('pokemon'));
        }
        return abort(404);
    }
}

上記の「重要!!!!」と書いていた箇所が肝です。
修正前のコントローラーだと、Mockもしくは実APIのクラスを直接書いていましたが、ここではPokemonRepositoryInterfaceというリポジトリを指定しています。
これで、ポケモン一覧・詳細画面が動いてしまいます!実際にMock・実APIの実行クラスはコントローラーに書いていないにもかかわらず、です。すごいですね。

解説

可能な限り難しい用語を使用せず、解説をしてみます。

コントローラーは、「どこから取得するか」はどうでもいい

これまで作成したPokemonControllerでは、ポケモン一覧画面を表示するための実装を行いました(詳細画面もですが、一旦一覧にフォーカスします)。
が、ここでちょっと考えてみましょう。
このポケモン一覧を「どこから」取得するかどうかは、コントローラー目線だとどうでも良かったりします。

APIから取得するか?Mockか?データベースか?ローカルに保存しているcsvか?
データの一覧を取得する際に考えられる候補はいくつかありますが、どれを採用したにしても、コントローラー目線だとどうでも良くて、
「どこかから取得したポケモン一覧を画面表示する」という処理が、コントローラーの役割です。

それを実現するために、PokemonRepositoryInterfaceが登場します。
このインタフェースでは、allメソッド、findメソッドが用意されています。
コントローラー目線だと、allメソッドを呼び出すことで、「どこかからポケモン一覧を取得」が実現できるわけですね。

イメージ図を作成しました。
PokemonController目線だと、PokemonRepositoryInterfaceさえ呼べていれば良いわけです。その先に何があるかどうかは、コントローラーからしたらどうでもいいわけですね。
pokemon3.png

コントローラーはポケモンの一覧を表示するために「ポケモンのデータがどこから来るのか?」を気にする必要はありません。
このように、データ取得の具体的な実装をコントローラーから切り離すことを「依存関係の分離」と呼びます。
これにより、柔軟にモックや実APIに切り替えることができ、開発やテストが効率的になります。

じゃあどこから呼び出すか?は、別の箇所で定義

とはいえ、「どこかからデータ一覧を取得する」ものの、 「じゃあどこから取得するの?」 という点は、指定してあげないといけません。何も指定しないと、コントローラーは困っちゃう(エラーになる)んですね。
それを指定してあげるのは、サービスプロバイダで指定している以下となります。

        $this->app->bind(PokemonRepositoryInterface::class, function ($app) {
            return config('app.use_mock_pokemon_api')
                ? new PokemonMockRepository()
                : new PokemonApiRepository();
        });

これはどういう記載かというと、

  • PokemonRepositoryInterfaceは、次のうちのどっちかだよ
  • configでuse_mock_pokemon_apiが設定されている(.envでUSE_MOCK_POKEMON_API=true)と、Mockを使用するよ
  • それ以外は、実APIを使用するよ

というものになります。
なので、.envファイルで設定を書き換えることによって、Mockと実APIを簡単に切り替えることができるわけですね。
このサービスプロバイダの記載を複数書くことによって、複数のコントローラーのMock・実APIをまとめて設定できます。

※ちなみに、Laravelの場合はサービスプロバイダに記載しますが、フレームワークによって記載方法は異なります。それぞれのフレームワークで調べてみてね。

自動テストでより便利

自動テストを実装する場合、このDIがより便利となります。
特に、APIなどの外部サービスを絡めた自動テストの場合、何も考えないと問題になるケースがあります。

  • 自動テストを行ったタイミングで、大量のAPIリクエストが実行され、API提供者から攻撃とみなされてしまう可能性
  • 課金式のAPIの場合、大量のAPIリクエストにより、高額の請求になってしまう可能性
  • データ登録があるAPIの場合、自動テストの度にAPIによりデータ登録が行われ、ゴミデータが大量に作成されてしまう可能性

さまざまな問題があります。
こんなときにどうするか?これを解決するのが、まさにDIです。
自動テストの時には、Mockを使用すれば良いわけなんです。

サービスプロバイダを、以下のように記載してください。

// AppServiceProvider(一部)
public function register()
    {
        $this->app->bind(PokemonRepositoryInterface::class, function ($app) {
            // テスト環境またはconfigで指定されている場合はMockリポジトリを使用
            if ($app->environment('testing') || config('app.use_mock_pokemon_api')) {
                return new PokemonMockRepository();
            }

            // それ以外の場合は実際のAPIリポジトリを使用
            return new PokemonApiRepository();
        });
    }

これで、「configによってMockを指定している場合」または「testingによる実行の場合」、Mockを使用します。
それ以外の場合、実APIを使用します。
切り分けが自動にできて便利ですね。

※ただし、Mockが完全に固定だと、戻りの値も固定となるため、テストの時に不便になる可能性があります。
「パラメータXXXXXが設定されている場合にはエラーとする」みたいに、ちょっとした動的制御はあっても良いかもしれないですね!

DIが便利なより具体的なケース

DIが便利になるケースはどんなものがあるか?気になる人もいると思います。
具体例をまとめてみました。
※ただし、自分の過去のプロジェクト経験を書くと、色々と支障出るかもしれないので、ここではChatGPTとPerplexity AIに聞いています。
皆様はどちらがお気に入りですか?自分は最近、Perplexity AI(ぱぷちゃん)がお気に入りです

ChatGPT回答結果

DI(依存性注入)が便利な具体的なケースはいくつかありますが、以下のような状況で特に有用です。

1. テストのしやすさ

テスト対象のクラスが外部サービスやデータベースなどに依存している場合、DIを使うとその依存部分をモックに置き換えることが容易です。たとえば、支払い処理クラスが外部の決済APIに依存している場合、DIを使ってモックの決済APIを注入すれば、外部APIに実際にアクセスすることなくテストが可能になります。これにより、テスト環境で予期せぬエラーが発生するリスクが減り、テストがより確実に行えます。

2. 設定の変更が容易

特定の設定(例えば、ログレベルや環境ごとのデータベース接続設定)を外部から注入できるようにしておくと、開発・テスト・本番環境ごとに異なる設定を容易に切り替えることができます。DIを使って設定オブジェクトを注入することで、コード変更なしに設定変更ができ、環境依存のコードが減ります。

3. 複雑な依存関係の管理

アプリケーションが大規模になると、依存関係が多くなり管理が困難になります。例えば、あるクラスが複数の他のクラスに依存し、それぞれの依存クラスもさらに別のクラスに依存している場合、DIを使うことで、依存関係の初期化や管理が自動化され、手動でインスタンスを生成する手間が省けます。結果として、コードの可読性が向上し、保守も楽になります。

4. プラグインや拡張の実装

アプリケーションの機能をプラグイン形式で拡張したい場合、DIを使うと動的に機能を追加できる仕組みを構築しやすくなります。例えば、様々な形式のファイルを処理するプログラムで、新しいファイルフォーマットに対応するパーサーをDIを通して簡単に追加できるようにすることで、プログラム全体を修正せずに機能追加が可能になります。

5. ライブラリの交換が容易

アプリケーションが特定のライブラリやフレームワークに強く依存している場合、DIを使ってその依存部分を抽象化しておくことで、将来的に別のライブラリに置き換えたいときもコードの修正箇所を最小限に抑えることができます。例えば、メール送信サービスを利用するアプリケーションで、異なるメール送信ライブラリに切り替える必要がある場合、DIを使ってメール送信サービスのインターフェースを注入することで、実装を差し替えるだけで対応できます。

DIは特に依存関係が複雑化しやすいシステムや、外部のサービスに依存する処理が多いアプリケーションで効果を発揮します。

Perplexity AI

DIが特に便利な具体的なケースをいくつか挙げます:

  1. データベースの切り替え:
    開発環境ではSQLiteを使用し、本番環境ではPostgreSQLを使用する場合。DIを使えば、データベース接続のインターフェースを定義し、環境に応じて適切な実装を注入できます。

  2. 外部APIの利用:
    決済サービスのAPIを利用する場合。開発中はモックAPIを使い、本番環境では実際のAPIを使用できます。DIにより、APIクライアントの実装を簡単に切り替えられます。

  3. ログ出力の制御:
    開発時はコンソールにログを出力し、本番環境ではファイルやデータベースにログを記録したい場合。ログ出力のインターフェースを定義し、環境に応じて適切なロガーを注入できます。

  4. 認証システムの変更:
    最初はシンプルな認証システムを使用し、後に多要素認証に移行する場合。認証サービスのインターフェースを定義しておけば、新しい認証システムへの移行が容易になります。

  5. キャッシュ戦略の変更:
    メモリキャッシュからRedisキャッシュに移行する場合。キャッシュのインターフェースを定義しておけば、キャッシュの実装を簡単に切り替えられます。

  6. 通知システムの拡張:
    最初はメール通知のみだったが、後にSMS通知やプッシュ通知を追加する場合。通知サービスのインターフェースを定義しておけば、新しい通知方法を簡単に追加できます。

  7. 設定の管理:
    設定をファイルから読み込む方式から、データベースやリモート設定サービスに移行する場合。設定プロバイダのインターフェースを定義しておけば、設定の取得方法を柔軟に変更できます。

  8. 機械学習モデルの切り替え:
    異なる機械学習モデルを試験的に導入する場合。予測サービスのインターフェースを定義しておけば、異なるモデルを簡単に切り替えて比較できます。

これらのケースでは、DIを使用することで、システムの柔軟性が大幅に向上し、将来の変更や拡張に対応しやすくなります。

まとめ

DIは、一見複雑に思えるかもしれませんが、開発を進める中での柔軟性を大きく向上させる強力な手法です。
特にAPI連携やテストの際には、モックと実サービスを簡単に切り替えることで、スムーズな開発が可能になります。
ぜひ、DIを活用して、効率的な開発を目指してみてください。

DIを使いこなすと、開発の幅が大きく広がっていきます。
慣れるまではメリットが分かりにくいかもしれないですが、慣れると、よりコードが綺麗に・便利になっていきます。
少しでも、皆様の理解の助けになっていれば嬉しいです!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?