245
203

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

最高にわかるDIコンテナ(特にPHPにフォーカスした

Last updated at Posted at 2019-06-02

DIとは

Dependency Injection(依存性の注入)の略です。
依存性の注入とはなんぞやということになりますよね。そもそもプログラムで依存するってどういうこと?

依存性って何

プログラムにおける依存性というのを簡単に説明すると、ファミコンなどのカセットが良い例です。

  • テトリスしか出来ないゲーム機
  • ファミコンなどのカセットを取り替えれるゲーム機

この2つのゲーム機があった時、テトリスしか出来ないゲーム機はテトリスというゲームにゲーム機が依存していることになります。
ファミコンだったらカセットを取り替えれるからゲーム機はゲームに依存してませんよね。カセットというインターフェースが同じものであれば基本的には利用できることになります。

テトリス専用ゲーム機の場合

まず、テトリス専用ゲーム機をプログラムでこれを例として示そうとするとこんな感じでしょうか。

class GameConsole {
    protected $game; 

    public function __construct() {
        $this->game = new Tetris;
    }
}

class Tetris {
}

$gameConsole = new GameConsole();
$gameConsole->run(); // GameConsoleクラスにはrunというメソッドが存在していると仮定します

GameConsoleクラスの中でTetrisクラスのインスタンスを作ってしまってます。
これでもしTetrisクラスが無かったらどうしましょう?new Tetrisでクラスが無いと言われエラーになってしまうでしょう。
そのため、GameConsoleクラスはTetrisクラスに依存していると言えます。(言い換えるとTetrisクラスが無いとGameConsoleクラスは作ることが出来ない)

ちょっと工夫したテトリス

テトリスは現状Tetrisクラスに依存してしまっていますが、ちょっと工夫することでそれを緩和することが出来ます。
まずは以下のソースと前のソースを比較してみてください。

class GameConsole {
    protected $game; 

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

class Tetris {
}

class SpeedUpTetris extends Tetris {
}

$gameConsole = new GameConsole(new Tetris);
$gameConsole->run();

$gameConsole2 = new GameConsole(new SpeedUpTetris);
$gameConsole->run();

このようにGameConsoleクラスの中でnewしなければ、Tetrisを元にしたゲームであれば導入することが出来ます。
DI(依存性の注入)とはこのようにクラスの中で作るのではなく、クラスの外で作って入れる(注入する)ことを言います。

ファミコンのようなゲーム機場合

ファミコンの場合をソースとして以下に書きました。
ファミコンはもっと依存性が低くなっています。

class GameConsole {
    protected $gameCard;

    public function __construct(GameCard $gameCard) {
        $this->gameCard = $gameCard
    }
}

interface GameCard {

}

class Tetris implements GameCard {

}
class Pinball implements GameCard {

}

$gameConsole = new GameConsole(new Tetris);
$gameConsole2 = new GameConsole(new Pinball);

ファミコンのようなゲーム機の場合、カセットというインターフェースさえ合っていればどんな種類のゲームでも遊ぶことが出来ます。
このような設計が出来ていると、GameCardインターフェースを実装したテスト用カセットを作ることも出来ますね。
また、今まではTetrisがGameConsoleに依存されていたわけですが

  • GameConsoleクラスはGameCardインターフェースに依存し、
  • TetrisやPinballもGameCardインターフェースに属する必要がある

という感じでGameConsole→Tetrisだった依存関係が、GameConsole→GameCard←Tetrisとなりました。Tetrisクラスの依存関係が逆転してます。これが俗にいうSOLID原則の依存性逆転の原則です。これはまあ知らなくても大丈夫なので補足といった感じです。

DIコンテナとは

さて、DIはクラスの中で作るのではなく、クラスの外で作って入れる(注入する)ことだということがわかったと思うので、DIコンテナの話をしていきましょう。

コンテナと言われると最近はDockerなどを想像してしまうかもしれませんが、ここでのコンテナの意味はものを入れれる箱といった意味で想像すると良いです。

ちょっと難しく言えば、依存関係を解決する箱、でしょうか。難しいですね。

DIコンテナでは無いがそれっぽい例

すごく簡単にDIコンテナっぽいものをソースで表してみます。
先程のファミコンのソースが先に存在していると想定して見てください。

$container = [];

$container['gameCard'] = function () {
    return Tetris;
}

$gameConsole = new GameConsole($container['gameCard']());

こんな感じでしょうか。先にコンテナという箱にgameCardという名前が来たらTetrisクラスを返すという定義をしておいて、実際に利用するときにはそのコンテナを経由して返すといった感じです。
正直これだけでは利点が謎かもしれませんが、利点はあります。Tetrisクラスを作る箇所がたくさんあったとしましょう。

$gameConsole = new GameConsole(new Tetris);
$gameConsole2 = new GameConsole(new Tetris);
$gameConsole3 = new GameConsole(new Tetris);
... // まあこのようなことは無いでしょうけど実際の作業ではクラスを複数回使うのは不思議でもありません。

毎回Tetrisクラスを作っています。そして突然、全部Pinballに変えてくれ!と言われたときです。
コンテナっぽいものを使わなかったら全部書き換えになってしまいます。

しかし、コンテナっぽいものを使っていたら

$container['gameCard'] = function () {
    return Pinball;
}

に変えるだけで済みますよね。
では、実際のDIコンテナについて少し解説していきます。

実際のDIコンテナ

ここでは例としてPimpleを上げます。
Laravel以外のフレームワークだと、結構service.phpみたいな名前のファイルにDIコンテナの定義がされていることが多いですね。

先程までのファミコンの例をこれに当てはめましょうか。

ファミコンの例に当てはめてみる

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

// gameCardサービスを定義する(DIコンテナに登録する)
$container['gameCard'] = function ($c) {
    // $cはDIコンテナです
    return new Tetris;
}

$gameConsole = new GameConsole($container['gameCard']);

となります。
ほとんど見た目は変わってないのですが、実際にはArrayAccessインターフェースを実装しているため内部ではsetされた時、getされた時で処理が書かれています。

setされる時はそこまででもないですが、内部の配列に情報の登録を行って
getされる時はsetした無名関数を実行した結果を用意したり、実際にsetされてるのかなどの処理がされます。

サービスをネストして定義する

ちなみに、こんな感じでネストした定義も可能です(よく使われます)

// (1)
$container['gameCard'] = function ($c) {
    return new Tetris;
}
// (2)
$container['gameConsole'] = function ($c) {
    return new GameConsole($c['gameCard']);
}

この場合、$container['gameConsole']を呼び出したら(2)の処理が実行され、$c['gameCard']で(1)が実行されてTetrisクラスのインスタンスが返ってきた後にGameConsoleクラスがインスタンス化されて結果がもらえます。

先程のDIコンテナっぽいものでは実現できなかったことができます。

毎回インスタンス化をする

Pimpleの場合だと、普通に登録されたサービスはシングルトン(1つのインスタンスを共有する)なため、取得する時に毎回インスタンスを作って欲しいときはfactoryを使います。

$container['gameCard'] = $container->factory(function ($c) {
    return new Tetris;
});

Laravelのコンストラクタインジェクション(メソッドインジェクション)

Laravelのサービスコンテナにはコンストラクタインジェクションという便利機能があります。
先程のファミコンの例を持ってきましょう。

class GameConsole {
    protected $gameCard;

    public function __construct(GameCard $gameCard) {
        $this->gameCard = $gameCard
    }
}

interface GameCard {

}

class Tetris implements GameCard {

}
class Pinball implements GameCard {

}

まずは、GameCardインターフェースがどのクラスを生成するかを定義しておきます。

app()->bind(GameCard::class, Tetris::class);

// 以下でもOK
app()->bind(GameCard::class, function($app) {
    return $app->make(Tetris::class);
});

bindメソッドはDIに登録するメソッドです。先程のコンテナ配列の中に入れる処理と同一です。
::classはPHPの機能で、完全修飾名を返します。完全修飾名というのは、\Foo\Barのように名前空間も含めた形のクラス名です。
実際の実務でも::classは多用しますので、覚えてると良いです。

そして、 GameConsoleクラスのインスタンスを生成するときは

$instance = app()->make(GameConsole::class);

となります。

「ん??」

となった方は頭が冴えてます。

先程のPimpleの例だと

$gameConsole = new GameConsole($container['gameCard']);

このように引数にインスタンスを渡す必要がありました。しかし、先程の記述には引数にインスタンスを渡すようなことをしていません。

Laravelの場合、GameConsoleクラスを生成する前にPHPのReflectionという機能を用いてGameConsoleクラスのコンストラクタの引数を確認します。

class GameConsole {
    protected $gameCard;

    // このGameCardという部分です
    public function __construct(GameCard $gameCard) {
        $this->gameCard = $gameCard
    }
}

Reflectionではパラメータのタイプヒンティングを確認できるので、GameCardインターフェースをGameConsoleクラスに入れる必要があることがわかります。
Laravelはこれを把握して、定義があるのであれば自動的に注入をやってくれます。もし定義が無かったとしても、インターフェースではなくクラスであれば自動的にインスタンスの作成までやってくれます。

class GameConsole {
    protected $gameCard;

    // Tetrisクラスが必ず注入される
    public function __construct(Tetris $gameCard) {
        $this->gameCard = $gameCard
    }
}

// この場合だと以下だけで自動的にTetrisクラスのインスタンスを作成してGameConsoleクラスのインスタンスが生成されます。
$instance = app()->make(GameConsole::class);

これは再帰的に実行されるので、例えばこの状態でTetrisクラスにも引数があってクラスやインターフェースが定義されていた場合も自動的に依存関係を解決してくれます。

特にLaravelはアプリケーションクラスそのものがDIコンテナであるため、コントローラーのインスタンスを作成する時からコンストラクタインジェクションが使われています。
同様に、メソッドに対してインジェクションができる機能をメソッドインジェクションと言います。

public function index(Request $request) {

}

このような記述をコントローラーで見たことがある人はいるでしょう。ここのRequestクラスはメソッドインジェクションで注入されてます。なので、書いていても書いていなくてもコントローラーのメソッドが動かせるのはLaravelの中で依存解決をしてくれているからですね~!

もっとLaravelのDIが書かれたサンプルがみたい方はlaravel/frameworkのリポジトリを見ると良いでしょう。○○○SercieProvider.phpといったファイルにてサービスコンテナの定義が書かれてあります。

最後に

途中から駆け足になってしまったのですが、DI、DIコンテナの解説とちょっとした利用方法を解説しましたがどうでしょうか?
不明点があったらコメントをしておいてくれると助かります。

そのうちいくつか要素を増やすかもですができる限りわかりやすいようにしていきたいなと思います。
ではー

245
203
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
245
203

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?