PHP
oop
DI
DependencyInjection
新人プログラマ応援

初学者でも5分で理解できるようにDI(Dependency Injection)を説明してみた

この記事の説明

この記事はプログラミング初学時に自分が知りたかった3点に絞り、
かつ可能な限り端的に説明をして概要を掴んで頂く為に書きました。

  • そもそもDIって?
  • どんなメリット(必要性)ある?
  • 具体的に言うと?

上記3点のみに絞って書いているので、かゆい所には手が届いておりません。
あしからず。

※ 今回はDIの説明に集中するため、DIコンテナ等の具体的な利用例については割愛しています。

そもそもDI(Dependency Injection)って?

一言で言うと、
『使いたいオブジェクトを渡してあげること』
そんだけです。

これだけでは逆にわかり辛いかもしれませんが、本当にそれだけです。
がんがん渡しましょう。

// 使いたいオブジェクト
$d = new Di();

// コンストラクタで渡した!
$obj = new Obj($d);

// セッターで渡した!
$obj = new Obj();
$obj->setDi($d)

// プロパティとして渡した!
$obj = new Obj();
$obj->di = $d

主に依存性の注入とか訳されているようです。

どんなメリット(必要性)ある?

一言で言うと、
『使う側と使われる側の関係が切り離しやすくなります』

使いたいオブジェクトを内部生成せずに外部から受け取っている為、
依存関係が低くなります(疎結合になる)

// DIじゃない夕食
class Dinner
{
    private $menu;

    public function __construct()
    {
        $this->menu = new Pasta();
    }

    public function eat()
    {
        echo $this->menu->getName() . 'を食べた!';
    }
}

$dinner = new Dinner();
$dinner->eat();    // パスタを食べた!

$dinner = new Dinner();
$dinner->eat();    // パスタを食べた!

$dinner = new Dinner();
$dinner->eat();    // パスタを食べた!

// 永遠にパスタしか食べれない

// DIな夕食
class Dinner
{
    private $menu;

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

    public function eat()
    {
        echo $this->menu->getName() . 'を食べた!';
    }
}

$dinner = new Dinner(new Pasta());
$dinner->eat();    // パスタを食べた!

$dinner = new Dinner(new Soba());
$dinner->eat();    // 蕎麦を食べた!

$dinner = new Dinner(new Tonkatsu());
$dinner->eat();    // トンカツを食べた!

// 日々の夕食に彩りが出て幸せ

夕食を食べる際、メニューに依存していない為、自由にメニューが選べます。

具体的に言うと?

上記2点のみでは抽象的過ぎて具体性がわかり辛いので具体的に言うと
必要に応じたオブジェクトを受け取って使う事により、使うオブジェクトの自由度が高くなります。

よくあげられる例として、

  • 単体テストが書きやすい
  • 環境の変更等に柔軟に対応しやすくなる

等があります。

上記の『DIじゃない夕食』の例で言うと、Dinnerクラスのテストをするには Pastaクラスの実装が終わっていないとできません。
『DIな夕食』の場合、Pastaクラスが完成していなくても 仮メニュークラスを用意する事でDinnerクラスのテストが可能になります。

その際に重要となるのはPastaクラスも仮メニュークラスも getNameメソッドを持っている事が約束されている事となります。
つまりは抽象化です。

それを踏まえて上記コードを付け足すと

// DIな夕食
// Menu インターフェース
interface Menu
{
    public function getName(): string;
}

// Menu インターフェースの具象クラス
class Pasta implements Menu
{
    public function getName(): string
    {
        // return 'パスタ'; 未完成
    }
}

// Menu インターフェースの具象クラス
class 仮メニュー implements Menu
{
    public function getName(): string
    {
        return 'お好み焼き';
    }
}

class Dinner
{
    private $menu;

    public function __construct(Menu $menu)    // Menu インターフェースの具象クラスを受け取る
    {
        $this->menu = $menu;
    }

    public function eat()
    {
        echo $this->menu->getName() . 'を食べた!';
    }
}

$dinner = new Dinner(new 仮メニュー());
$this->assertSame('お好み焼きを食べた!', $dinner->eat());    // Pastaクラス未実装でもDinnerクラスのテストができた

もちろんテスト以外にも様々なメリットがあります(が、パっと思いつかなかったので一つだけ例を)

// 抽象化
Interface Datasource
{
    public function save(Data $data);
}

// 具象クラス
class MySql implements Datasource
{
    public function save(Data $data)
    {
        // MySqlに保存する処理
    }
}

// 具象クラス
class PostgreSql implements Datasource
{
    public function save(Data $data)
    {
        // PostgreSqlに保存する処理
    }
}

// 使うクラス
class Model
{
    private $db;

    public function __construct(Datasource $db)    // Datasourceインターフェースの具象クラスを受け取る
    {
        $this->db = $db;
    }

    public function save()
    {
        $this->db->save(new Data(1));
    }
}

// データが入るクラス
class Data {
   private $id;

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

// MySQLに保存するぞー
$model = new Model(new MySql());
$model->save();


// 今日からPostgreSQL使うんだよ
$model = new Model(new PostgreSql());
$model->save();

こんな感じで使用する(抽象化された)クラスを外から渡して上げることにより自由度が増します。

ただ、お気づきのようにこのままでは渡すコードがベタ書きされているので、決して本当の意味で使いやすい形になったとまでは言えません。

そこでDIコンテナ等の仕組みを使うことで依存性の注入部分を自動化することができます。

が、それはまた別のお話と言うことで。

まとめ

お腹空いた