依存性の注入とは

Laravel のドキュメントを見ていると「依存性の注入」という言葉が良く出てきます。依存性の注入は Dependency Injectionと言われ、「DI」と表記されます。また、別の表現では Inversion of Control(制御の反転)とも言われ、「IoC」と表記されます。Laravel のドキュメントでは「 loC」の方が使われています。

Laravel では、依存性の注入という概念を核としてフレームワークが作成されています。この概念を理解することが、Laravel を理解することへの近道になるかと思います。

この依存性の注入とは何でしょうか?
今回は簡単なPHPのサンプルで、この概念を理解したいと思います。
※ Laravelを使ったソースコードでは無いのでご注意ください。


依存性の注入を使っていないケース

<?php
// send_message.php

/*
 * Class
 */

interface SenderInterface {
    public function send($message);
}

class MailSender implements SenderInterface {  // ①
    public function send($message) {
        // ここで、メールでメッセージを送る

        return "メールで送りました。";
    }
}

class BikeSender implements SenderInterface {  // ②
    public function send($message) {
        // ここで、バイクに乗ってメッセージを届ける

        return "バイク便で届けました。";
    }
}

class Messenger {
    protected $sender;

    public function __construct() {
        $this->sender = new MailSender();  // ③
    }

    public function send($message) {
        return $this->sender->send($message);
    }
}


/*
 * Main
 */

// メールで送る
$messenger = new Messenger();  // ④
echo $messenger->send("合格通知") . "\n";  // "メールで送りました。"

SenderInterface を持つ、①MailSenderと②BikeSenderクラスがあります。
③Messengerクラスではコンストラクタで直接 MailSenderクラスをnewしています。
その為、④Main プログラムで、送信手段をメールからバイク便に変更しようとすると、③Messengerクラスのコンストラクタを修正する必要があります。

この状態を「Messengerクラスが、MailSenderクラスに依存している」と言います。


依存性の注入を使ったケース

<?php
// send_message_di.php

/*
 * Class
 */

interface SenderInterface {
    public function send($message);
}

class MailSender implements SenderInterface {  // ①
    public function send($message) {
        // ここで、メールでメッセージを送る

        return "メールで送りました。";
    }
}

class BikeSender implements SenderInterface {  // ②
    public function send($message) {
        // ここで、バイクに乗ってメッセージを届ける

        return "バイク便で届けました。";
    }
}

class Messenger {
    protected $sender;

    public function __construct(SenderInterface $sender) {  // ③
        $this->sender = $sender;
    }

    public function send($message) {
        return $this->sender->send($message);
    }
}


/*
 * Main
 */

// メールで送る
$messenger = new Messenger(new MailSender());  // ④
echo $messenger->send("合格通知") . "\n";  // "メールで送りました。"

// バイク便で送る
$messenger = new Messenger(new BikeSender());  // ⑤
echo $messenger->send("合格通知") . "\n";  // "バイク便で届けました。"

③Messengerクラスのコンストラクタで、SenderInterfaceのクラスを取得するよう変更しました。これで特定のクラスへの依存を排除できました。
④⑤Main プログラムで送信手段を変更する事が出来るようになりました。

Messengerクラスの依存性を外部から注入できるようになりました。送信手段を変更したい場合は、Messangerクラスを利用する側で送信手段をnewして注入します。こうすることで、送信手段を変更する為に、Messangerクラスを修正することは無くなりました。Messengerクラスは他のクラスと疎結合になり、メンテナンス性が増しました。

しかし、いくつか問題が残っています。
ここまでは、具象クラスに依存する箇所を利用者であるMain部分に寄せることで、Messengerクラスの保守性を上げてきました。しかし、Main部分はどうでしょうか?

  • Messengerクラスを使う部分が増えると、具象クラスをnewする箇所が多くなり、メンテンスが大変
  • MailSenderやBikeSenderクラスが他のクラスに依存し、構造を持った場合に、多段階にnewして依存注入するのが大変
  • MailSenderやBikeSenderクラスの構造に変更があった時に、メンテナンスが大変

そこで、これらの問題を解決する為にDIコンテナが登場します。


DI コンテナを使ったケース

さらに一歩進めて、DI コンテナという仕組みを導入してみます。
この例では DI コンテナに Pimple を使用します。

https://github.com/silexphp/Pimple

まず、composerでカレントディレクトリにPimpleをインストールします。

composer require pimple/pimple:~3.0

DI コンテナを使ったサンプル

<?php
// send_message_di_container.php

require "vendor/autoload.php";  // ① Pimpleをオートロード

/*
 * Class
 */

interface SenderInterface {
    public function send($message);
}

class MailSender implements SenderInterface {
    public function send($message) {
        // ここで、メールでメッセージを送る

        return "メールで送りました。";
    }
}

class GMailSender implements SenderInterface {  // ②
    public function send($message) {
        // ここで、GMailでメッセージを送る

        return "GMailで送りました。";
    }
}

class BikeSender implements SenderInterface {
    public function send($message) {
        // ここで、バイクに乗ってメッセージを届ける

        return "バイク便で届けました。";
    }
}

class Messenger {
    protected $sender;

    public function __construct(SenderInterface $sender) {
        $this->sender = $sender;
    }

    public function send($message) {
        return $this->sender->send($message);
    }
}

/*
 * Config(DIコンテナにバインドすると言う)
 */

$container = new Pimple\Container();        // ③ DIコンテナ

$container['mail'] = function($container) {  // ④
    // return new MailSender();
    return new GMailSender();
};

$container['bike'] = function($container) {  // ⑤
    return new BikeSender();
};


/*
 * Main
 */

// メールで送る
$messenger = new Messenger($container['mail']);  // ⑥
echo $messenger->send("合格通知") . "\n";

// バイク便で送る
$messenger = new Messenger($container['bike']);  // ⑦
echo $messenger->send("合格通知") . "\n";

①composerでインストールしたPimpleをオートロードします。
②GMailSenderを追加しました。
③DIコンテナとしてPimpleをnewします
④⑤ ‘mail’, ‘bike’ というキーワードに対してどのクラスを生成するかの設定をします。
⑥⑦DIコンテナにキーワードを渡すことでSenderInterfaceを持つクラスを生成します。

Configとして、DIコンテナにクラスの生成を委譲することで、Mainプログラム部分も依存性を排除することができました。メールで送信する時の実際の手段を②GMailSenderに変更する時は、④DIコンテナを修正します。

DIコンテナを導入したことで、MessengerクラスとSenderInterfaceを持つクラスの生成を1箇所に集約することができました。また、MailSenderやBikeSenderクラスが他のクラスに依存するなどして、構造に変更があった場合でも、DIコンテナへのバインド部分(Config部分)だけを修正すればよくなります。Main部分において、どのようにクラスをnewするかといったクラス構造は隠蔽されます。

また、別の効果として、テストコードを書く時に、クラスをモックやスタブに簡単に入替えることも可能になります。

DIコンテナのメリット

  • クラスの生成を1箇所に集約できる
  • クラスの構造を隠蔽できる
  • モックを使ってテストがしやすくなる

→ 保守性、柔軟性、テスト性が向上する

上記のサンプルでは Mainプログラムの部分のコードが少ないので、DIコンテナの恩恵があまり感じられないかもしれませんが、コードがもっと増えて、クラスをnewする箇所があっちこっちに出てくるようであれば、このDIコンテナの導入が非常に有効になってきます。


まとめ

依存性の注入のイメージはなんとなく掴めたでしょうか?
今回のサンプルではDI コンテナとして Pimpleを使いましたが、Laravel では Illiminate\Foundation\Application クラスが DI コンテナにあたります。DI コンテナを中心としてフレームワークが作られています。

DIコンテナを導入すると、多少複雑にはなりますが、保守性、柔軟性、テスト性は飛躍的に向上します。

依存性の注入とは」への1件のフィードバック

コメントを残す