依存性の注入 (Dependency Injection)
CoddyのPHPジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 57/91。
前のレッスンでは、コンポジションによってCarがEngineオブジェクトを保持できる方法を見ました。しかし、クラスの内部で依存関係を作成することには問題があります。それは、コードを硬直化させ、テストを困難にすることです。依存性の注入(Dependency Injection)は、依存関係を内部で作成するのではなく、外部から渡すことでこの問題を解決します。
これら2つのアプローチを比較してみましょう:
<?php
// DIなし - 内部で依存関係が作成される
class Car {
private Engine $engine;
public function __construct() {
$this->engine = new Engine(); // 密結合
}
}
// DIあり - 依存関係が渡される
class Car {
public function __construct(private Engine $engine) {}
}
$engine = new Engine();
$car = new Car($engine); // 外部から注入される
第2のアプローチは依存性の注入(Dependency Injection)です。Carは自分自身でエンジンを作成するのではなく、エンジンを受け取ります。この単純な変更には強力な利点があります。異なるエンジンの型を渡したり、テストのために実装を入れ替えたりすることができ、クラスの柔軟性が高まります。
さらに高い柔軟性を得るために、具象クラスの代わりにインターフェースを注入します:
<?php
interface EngineInterface {
public function start(): string;
}
class Car {
public function __construct(private EngineInterface $engine) {}
public function start(): string {
return $this->engine->start();
}
}
これで、CarはEngineInterfaceを実装する任意のクラス(ガソリンエンジン、電気モーター、またはテスト用のモックなど)で動作するようになります。この疎結合化こそが、メンテナンス性が高くテスト可能なPHPアプリケーションを作成する上で、依存性の注入が不可欠である理由です。
チャレンジ
簡単依存性の注入(Dependency Injection)の威力を示す通知システムを構築しましょう。メッセージの送信方法をハードコーディングする代わりに、配信方法を簡単に切り替えられる柔軟なシステムを作成します。
コードは以下の4つのファイルに分けて構成します:
MessageSenderInterface.php—send(string $recipient, string $message): stringという単一のメソッドを持つMessageSenderInterfaceというインターフェースを定義します。このインターフェースは、すべてのメッセージ送信者が従うべき規約を確立します。EmailSender.php—MessageSenderInterfaceを実装するEmailSenderクラスを作成します。インターフェースファイルをインクルードしてください。send()メソッドは"Email to $recipient: $message"を返す必要があります。NotificationService.php— コンストラクタを通じて依存関係を受け取るNotificationServiceクラスを作成します。インターフェースファイルをインクルードしてください。このクラスは以下の要件を満たす必要があります:- コンストラクタのプロパティ昇格(constructor promotion)を使用して、コンストラクタで
MessageSenderInterfaceを受け取る - 注入された送信者に処理を委譲し、その結果を返す
notify(string $recipient, string $message)メソッドを持つ
NotificationServiceが具体的なクラスではなく、インターフェースに依存している点に注目してください。これにより、サービスを変更することなく、インターフェースを実装する任意の送信者を注入できるようになります。- コンストラクタのプロパティ昇格(constructor promotion)を使用して、コンストラクタで
main.php— EmailSender と NotificationService のファイルをインクルードします。受信者(recipient)とメッセージ(message)の2つの入力を受け取ります。EmailSenderを作成し、それをNotificationServiceに注入してから、notify()を呼び出して結果を出力してください。
このパターンにより、NotificationService の柔軟性が保たれます。メール、SMS、あるいはまだ作成していない他の送信方法であっても、注入する送信者に応じて動作します。サービスは自ら依存関係を作成せず、外部からそれらを受け取ります。
チートシート
依存性の注入(DI)は、依存関係を内部で作成するのではなく、外部からクラスに渡す手法です。
DIなし(密結合):
<?php
class Car {
private Engine $engine;
public function __construct() {
$this->engine = new Engine(); // 内部で作成
}
}DIあり(疎結合):
<?php
class Car {
public function __construct(private Engine $engine) {}
}
$engine = new Engine();
$car = new Car($engine); // 外部から注入柔軟性を最大限に高めるには、具体的なクラスではなくインターフェースを注入します:
<?php
interface EngineInterface {
public function start(): string;
}
class Car {
public function __construct(private EngineInterface $engine) {}
public function start(): string {
return $this->engine->start();
}
}これにより、Car は EngineInterface を実装する任意のクラスで動作できるようになり、コードの保守性とテストのしやすさが向上します。
自分で試してみよう
<?php
require_once 'EmailSender.php';
require_once 'NotificationService.php';
// 入力を読み込む
$recipient = trim(fgets(STDIN));
$message = trim(fgets(STDIN));
// TODO: EmailSenderインスタンスを作成する
// TODO: それをNotificationServiceに注入する
// TODO: notify()を呼び出し、結果を出力する
?>このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
10オブジェクト指向の高度な概念
コンポジション vs 継承依存性の注入 (Dependency Injection)匿名クラスEnum (PHP 8.1)ファイバー (PHP 8.1)オブジェクトのクローン徹底解説ジェネレータとイテレータ