DIとは?DIコンテナとは?試してみた(後編)[PHP][Pimple][DI]
DIとは?DIコンテナとは?試してみた(前編)[PHP][DI] - あざらし備忘録。 の後編です。
今回はDIコンテナについて。
DIコンテナとは
DIをするにあたって、使用する度に毎回依存性のあるものを生成して注入して、...というのが面倒くさいのでその依存性を定義してコンテナという形で保管しておくことでそのコンテナを取り出すだけで依存関係の整ったものを手に入れることができる代物です。
前回のおさらい
前回、DIの例として書いたコードは以下の様でした。
<?php interface SongInterface { public function getTitle(); // ...など曲に関する諸々 } class Song implements SongInterface { public function __construct($title=null) { $this->title = $title; } public function getTitle() { return $this->title; } // ... } interface MusicPlayerInterface { public function play(); } class MusicPlayer implements MusicPlayerInterface { /** * @var SongInterface */ private $song; public function __construct(SongInterface $song) { $this->song = $song; } public function play() { echo '"'.$this->song->getTitle().'"を再生するよ'; } } interface TwitterClientInterface { public function post(); } class TwitterClient implements TwitterClientInterface { public function post() { // 投稿処理 echo '音ゲーをプレイしたよとつぶやくよ'; } } class Otoge { /** * @var SongInterface */ private $song; /** * @var MusicPlayerInterface */ private $music_player; /** * @var TwitterClientInterface */ private $twitter_client; public function __construct(SongInterface $song, MusicPlayerInterface $music_player, TwitterClientInterface $twitter_client) { $this->song = $song; $this->music_player = $music_player; $this->twitter_client = $twitter_client; } public function play() { echo '"'.$this->song->getTitle().'"で音ゲーを開始するよ'; $this->music_player->play(); } public function tweet() { $this->twitter_client->post(); } }
実行例は、こんな感じでした。
<?php $song = new Song('test'); $music_player = new MusicPlayer($song); $twitter_client = new TwitterClient(); $otoge = new Otoge($song, $music_player, $twitter_client); $otoge->play(); $otoge->tweet();
songもmusic_playerもtwitter_clientも...毎回インスタンスを生成してDIして、...面倒くさそうですね。
DIコンテナを使ってみる
今回は、PHPの中で最も有名なDIコンテナの一つであるPimpleを使ってサンプルを作ってみます。
Pimple - A simple PHP Dependency Injection Container
早速ですがこちら。
クラス/インターフェース定義については変更はありません。実行例は次のようになります。
<?php require_once './config_container.php'; $container['song.title'] = 'hoge'; $otoge = $container['otoge']; $otoge->play(); $otoge->tweet();
先ほどのDIの例ではあったsongやmusic_playerやtwitter_clientといったotogeに依存関係のあった面倒くさいインスタンスの生成がぱっと見なくなっています。 また、containerという見慣れないもの見えますね。
このcontainerというやつがよしなにいい感じにしてくれているようです。
まぁもうちょっと言うと名前と文脈からDIコンテナですねw
原理はわからないですが呼び出し側としては曲情報(今回はtitleだけだけど)をセットするだけでその曲情報を持った状態の$otogeを作ることができ、また、表では特に何もしていないのにtweet機能やplay機能を扱う事ができるようになり、非常に使いやすくなりました。
原理を見てみる
requireしているconfig_container.phpを見てみましょう。
<?php use Pimple\Container; $container = new Container(); $container['song.title'] = ''; $container['song'] = $container->factory(function ($c) { return new Song($c['song.title']); }); $container['music_player'] = $container->factory(function ($c) { return new MusicPlayer($c['song']); }); $container['twitter_client'] = function ($c) { return new TwitterClient(); }; $container['otoge'] = $container->factory(function ($c) { return new Otoge($c['song'], $c['music_player'], $c['twitter_client']); });
これがDIコンテナ設定の中身です。
ここで依存関係を定義しています。
仕組みとしては、$container['otoge']が実行時に呼ばれることで無名関数が呼ばれてその際にインスタンスが生成されるという寸法です。
Pimpleについては一DIコンテナ例として用いただけなので詳しい説明は割愛しますが、
factory()を用いることで無名関数が呼ばれる度に新たなインスタンスを生成します。
用いない場合は無名関数が一度呼ばれた時にキャッシュし、以後同一インスタンスを返すいわゆるシングルトンな振る舞いをします。
メール配信やツイッター送信など、サービスレイヤーなものについてはfactory()なしで呼ぶことで無駄にインスタンス生成することなく扱え、DIコンテナとしてはそのような振る舞いを期待して用いることが多いかと思います(例ではfactory()が多めになってしまってアレですが。。。)
また、$container['song.title']のように普通に文字列等も扱うことはできます。
このように、DIコンテナを上手く使うことで呼び出し側としては非常にスッキリとした呼び出し方をすることができ、意図のわかりやすいコードが書けるようになります。
呼び出し側がすっきりするのは非常に良い!
DIコンテナを使ってあげることで呼び出し側が非常に見通しが良くなり何をしたいのかが明確になるという恩恵が得られるのは非常に大きいなぁと感じました。
上手に付き合っていきたい代物ですね!
今回用いたサンプルコードは以下に置いてありますのでよろしければ。