はじめに
プログラミングには「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>
上記のようなコードとなります。これで、ポケモンの一覧を表示できます。
以下のような画面ですね。(びっくりするぐらいシンプルな画面ですが・・・!笑)
モックと実APIの切り替え
2ヶ月後、APIの連携が可能になった時点で、モックから実際のAPIに切り替える方法は非常に簡単です。
以下のような手順で実装できます。
-
PokemonApiRepository
クラスを実装 - コントローラーでリポジトリを変更
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さえ呼べていれば良いわけです。その先に何があるかどうかは、コントローラーからしたらどうでもいいわけですね。
コントローラーはポケモンの一覧を表示するために「ポケモンのデータがどこから来るのか?」を気にする必要はありません。
このように、データ取得の具体的な実装をコントローラーから切り離すことを「依存関係の分離」と呼びます。
これにより、柔軟にモックや実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が特に便利な具体的なケースをいくつか挙げます:
-
データベースの切り替え:
開発環境ではSQLiteを使用し、本番環境ではPostgreSQLを使用する場合。DIを使えば、データベース接続のインターフェースを定義し、環境に応じて適切な実装を注入できます。 -
外部APIの利用:
決済サービスのAPIを利用する場合。開発中はモックAPIを使い、本番環境では実際のAPIを使用できます。DIにより、APIクライアントの実装を簡単に切り替えられます。 -
ログ出力の制御:
開発時はコンソールにログを出力し、本番環境ではファイルやデータベースにログを記録したい場合。ログ出力のインターフェースを定義し、環境に応じて適切なロガーを注入できます。 -
認証システムの変更:
最初はシンプルな認証システムを使用し、後に多要素認証に移行する場合。認証サービスのインターフェースを定義しておけば、新しい認証システムへの移行が容易になります。 -
キャッシュ戦略の変更:
メモリキャッシュからRedisキャッシュに移行する場合。キャッシュのインターフェースを定義しておけば、キャッシュの実装を簡単に切り替えられます。 -
通知システムの拡張:
最初はメール通知のみだったが、後にSMS通知やプッシュ通知を追加する場合。通知サービスのインターフェースを定義しておけば、新しい通知方法を簡単に追加できます。 -
設定の管理:
設定をファイルから読み込む方式から、データベースやリモート設定サービスに移行する場合。設定プロバイダのインターフェースを定義しておけば、設定の取得方法を柔軟に変更できます。 -
機械学習モデルの切り替え:
異なる機械学習モデルを試験的に導入する場合。予測サービスのインターフェースを定義しておけば、異なるモデルを簡単に切り替えて比較できます。
これらのケースでは、DIを使用することで、システムの柔軟性が大幅に向上し、将来の変更や拡張に対応しやすくなります。
まとめ
DIは、一見複雑に思えるかもしれませんが、開発を進める中での柔軟性を大きく向上させる強力な手法です。
特にAPI連携やテストの際には、モックと実サービスを簡単に切り替えることで、スムーズな開発が可能になります。
ぜひ、DIを活用して、効率的な開発を目指してみてください。
DIを使いこなすと、開発の幅が大きく広がっていきます。
慣れるまではメリットが分かりにくいかもしれないですが、慣れると、よりコードが綺麗に・便利になっていきます。
少しでも、皆様の理解の助けになっていれば嬉しいです!