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件のフィードバック