Menu
Coddy logo textTech

依存性の注入 (Dependency Injection)

CoddyのPHPジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 57/91。

前のレッスンでは、コンポジションによってCarEngineオブジェクトを保持できる方法を見ました。しかし、クラスの内部で依存関係を作成することには問題があります。それは、コードを硬直化させ、テストを困難にすることです。依存性の注入(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();
    }
}

これで、CarEngineInterfaceを実装する任意のクラス(ガソリンエンジン、電気モーター、またはテスト用のモックなど)で動作するようになります。この疎結合化こそが、メンテナンス性が高くテスト可能なPHPアプリケーションを作成する上で、依存性の注入が不可欠である理由です。

challenge icon

チャレンジ

簡単

依存性の注入(Dependency Injection)の威力を示す通知システムを構築しましょう。メッセージの送信方法をハードコーディングする代わりに、配信方法を簡単に切り替えられる柔軟なシステムを作成します。

コードは以下の4つのファイルに分けて構成します:

  • MessageSenderInterface.phpsend(string $recipient, string $message): string という単一のメソッドを持つ MessageSenderInterface というインターフェースを定義します。このインターフェースは、すべてのメッセージ送信者が従うべき規約を確立します。
  • EmailSender.phpMessageSenderInterface を実装する EmailSender クラスを作成します。インターフェースファイルをインクルードしてください。send() メソッドは "Email to $recipient: $message" を返す必要があります。
  • NotificationService.php — コンストラクタを通じて依存関係を受け取る NotificationService クラスを作成します。インターフェースファイルをインクルードしてください。このクラスは以下の要件を満たす必要があります:
    • コンストラクタのプロパティ昇格(constructor promotion)を使用して、コンストラクタで MessageSenderInterface を受け取る
    • 注入された送信者に処理を委譲し、その結果を返す notify(string $recipient, string $message) メソッドを持つ
    NotificationService が具体的なクラスではなく、インターフェースに依存している点に注目してください。これにより、サービスを変更することなく、インターフェースを実装する任意の送信者を注入できるようになります。
  • 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();
    }
}

これにより、CarEngineInterface を実装する任意のクラスで動作できるようになり、コードの保守性とテストのしやすさが向上します。

自分で試してみよう

<?php
require_once 'EmailSender.php';
require_once 'NotificationService.php';

// 入力を読み込む
$recipient = trim(fgets(STDIN));
$message = trim(fgets(STDIN));

// TODO: EmailSenderインスタンスを作成する
// TODO: それをNotificationServiceに注入する
// TODO: notify()を呼び出し、結果を出力する

?>
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン