あざらし備忘録。

渋谷ではたらく音ゲー大好きウェッブエンジニアがいろいろ思った事やった事を書いていくブログです

DIとは?DIコンテナとは?試してみた(前編)[PHP][DI]

DIはダイジだーDIコンテナはベンリだーとかは色々聴いていたのですが、DIもDIコンテナも何もわかっていないので勉強してみようと思い少し試してみたのでメモ。

まずはDI編。

DIとは

Dependency Injectionです。

依存性の注入です。

...?(´・ω・`)

依存性?(´・ω・)注入?(´・ω・)っていう感じなのでもう少し詳しく深掘ってみます。

依存性とは

「クラスAを正しく動かすためには、クラスBが既に出来上がってないとだめ」という状態を、「クラスAはクラスBに依存している」といいます。

例を示します。(もろもろ言うことはあれどとりあえず依存している事がわかる実装になっていればよしとします)

<?php

class Song
{
    public function __construct($title=null)
    {
        $this->title = $title;
    }
    public function getTitle()
    {
        return $this->title;
    }
}

class MusicPlayer
{
    private $song;

    public function __construct()
    {
        $this->song = new Song('test');
    }

    public function play()
    {
        echo '"'.$this->song->getTitle().'"を再生するよ';
    }
}

class TwitterClient
{
    public function post()
    {
        // 投稿処理
        echo '音ゲーをプレイしたよとつぶやくよ';
    }
}

class Otoge
{
    private $song;
    private $music_player;
    private $twitter_client;

    public function __construct()
    {
        $this->song = new Song('test');
        $this->music_player = new MusicPlayer();
        $this->twitter_client = new TwitterClient();
    }

    public function play()
    {
        echo '"'.$this->song->getTitle().'"で音ゲーを開始するよ';
        $this->music_player->play();
        // その他得点記録したりとかの処理
    }

    public function tweet()
    {
        $this->twitter_client->post();
    }
}

実行例はこんな感じです。

<?php 

$otoge = new Otoge();
$otoge->play();
$otoge->tweet();

この例では、

  • 音ゲー内で曲情報を扱いたい
  • 音ゲー内で音楽を再生したい
  • 音ゲー内からツイートを投稿したい
  • 音楽プレイヤー内でも曲情報を扱いたい

という要件があるとします。

この例だと、次のような依存関係があると言えます。

  • クラスOtogeはクラスSongに依存している
  • クラスOtogeはクラスMusicPlayerに依存している
  • クラスOtogeはクラスTwitterClientに依存している
  • クラスMusicPlayerはクラスSongに依存している

依存自体は悪いことではありません。要件があって役割に応じてクラスを分けていったら自然と要件上依存関係にあるクラスたちは生まれてくるでしょう。

じゃあ何がいけないのか

依存関係が「ベタ書き」されていることです。

クラスOtogeのコンストラクタに注目して欲しいのですが、コンストラクタ内でSong, MusicPlayer, TwitterClientのインスタンスが生成されてプロパティにセットされていますね。

また、クラスMusicPlayerのコンストラクタでも、Songのインスタンスが生成されてプロパティにセットされています。

これは依存しているクラスに依存関係が「ベタ書き」されていると言えるでしょう。(大事なことなので2回(ry)

この状態だと、以下のようなよろしくない点が考えられます。

  • Song, MusicPlayer, TwitterClientを別のものに替えたいときにOtogeに修正を加えなければならなくなる
  • Song, MusicPlayer, TwitterClientが上手く動かないとOtogeに対してのテストができなくなる
  • Song, MusicPlayer, TwitterClientが出来上がるまでOtogeの開発ができなくなる
  • Songを別のものに替えたいときにMusicPlayerに修正を加えなければならなくなる
  • Songが上手く動かないとMusicPlayerに対してのテストができなくなる
  • Songが出来上がるまでMusicPlayerの開発ができなくなる
  • Otoge->tweet()をテストするたびツイッターに投稿されてしまう
  • MusicPlayer->play()をテストするたび音楽が再生されてしまう

こういうよろしくない点が出てきてしまうのはまずいので、なんとかしたいですね。

じゃあどうしたらいいのか

諸悪の根源は依存関係が「ベタ書き」されていることです。(大事なことなので3回(ry)

なので「ベタ書き」しないようにしましょう。

「ベタ書き」しない = クラス内でインスタンスを生成しない

つまり

「ベタ書き」しない = クラス外でインスタンスを生成して外から渡してあげる

といえます。

これを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();

DIしていない状態から変わったことは、以下のような点です。

こうすると、OtogeやMusicPlayerのインスタンスを生成する際に引数で必要なものを注入してあげればいいので、依存性を「ベタ書き」することは回避できました!

なお、Interfaceは必ずしも実装しなさい、というわけではありません。

実装を知ってしまっていても良い場合はクラスでやってしまって、外部に公開する必要があるような部分はinterfaceでやってしまう、というのもよくあります。

今回はDIをわかりやすくするためにすべてinterfaceを実装していますが、適宜使用するようにすると修正の手間が少なくなってより効率よく開発が行えるかと思います。

DIしていくことの副作用

依存性を注入できるようになったわけですが、気になる点がひとつ。

「引数爆発しません?」

そうですよね。依存性が生まれるたびに引数がひとつ増えて...となったら引数が多くなって管理するのが面倒になってきそうです。

呼び出し先での実装もいちいち各インスタンスを生成していって面倒ですし「俺はOtogeクラスのインスタンスを作りたかっただけなのに...」ってなりそうですね。

そこでDI「コンテナ」の登場

そんな悩みが、DIコンテナ、というものを使うとすっきりして管理が楽になります。

後編ではPHPDIコンテナの一つであるPimpleを用いてDIコンテナについて書いていきたいと思います。

後編はこちら

DIとは?DIコンテナとは?試してみた(後編)[PHP][Pimple][DI] - あざらし備忘録。