PHP
DI

DI・DIコンテナ、ちゃんと理解出来てる・・?

More than 1 year has passed since last update.

意外と分からずに、「とりあえず」とか「なんとなく」で使っちゃうパターンが多い系案件な気がして書いてみます。

こんな事ありませんか?


  • DIとDIコンテナの違いを説明出来ない

  • DIとサービスロケータの違いを説明出来ない

  • DIを使ってるつもりが、サービスロケータになっている

  • DI、サービスロケータが、ただの「パターン」の1つであることを理解してない


DI(Dependency Injection)を正しく理解する

そもそも、Dependeny Injectionを日本語にするとどういう意味になるでしょうか。

多くの人が「依存性の注入」とか応えるのではないでしょうか?

私もそうでした。きっと何かで読んだのでしょう。

(wikipediaに「依存性の注入」と書いてありますね)


補足

なぜ依存性を注入してあげると良いのか、そのメリット等は後述しますが、

DIというのはただのパターンの1つです。

たまにDIコンテナとDIを同一視して使ってるのを見ますが、DIはパターン、

DIコンテナはDI実現をお手伝いするためのフレームワークです。

つまり、別にDIコンテナを使わなくてもDIは実現可能なのです。


DIって本当はどういう意味なのか?

元の英語版のwikipediaにはこう書いてありました。


A dependency is an object that can be used (a service).


「Dependency」(依存性と訳していた)は、「オブジェクト」です。

つまり、DIとは、「依存性の注入」じゃなくて、「オブジェクトの注入」だった訳ですね。

この時点で、「依存性の注入ってなんだよw」感から開放されました。

オブジェクトならいつも使ってるから分かってますしね。


DIじゃない場合

Sampleクラスで、ログを仕込む例を考えてみましょう。

DIじゃないとこうなります。

class Sample

{
const LOG_FILE_PATH = 'sample.log';

private $logger = null;

public function __construct()
{
$this->logger = new FileLogger();
$this->logger->setFilePath(self::LOG_FILE_PATH);
}

public function doSomething()
{
// 何か処理
// ログを残す
$this->logger->info('doSomething successed!');
}
}

コンストラクタの中で、ログを吐く為のFileLoggerをnewしてますね。


DIの場合

class Sample

{
private $logger = null;

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

public function doSomething()
{
// 何か処理
// ログを残す
$this->logger->info('doSomething successed!');
}
}

これだけです。

Sampleには外から「オブジェクト($logger)を注入」してやるだけ。


え、何が良いの?

DIとDIじゃない例を比較して見ます。

DIじゃない例
DIな例

FileLoggerが実装出来るまで、Sampleが実装出来ない
FileLoggerがなくてもSampleの実装を進められる

FileLoggerからDatabaseLoggerに変える場合、Sampleを修正する必要がある
Sampleの呼び出し元を変えるだけでOK

SampleのdoSomethingのテストの度に、ファイルにログが吐かれる
モックを渡せば良いので、実際にファイルにログは吐かれない

色々便利ですね。

因みに、今回はコンストラクタでDIしましたが、これはコンストラクタインジェクションといい、他にもセッターインジェクション、メソッドインジェクション等、コンストラクタ以外から注入する方法もあります。


でも、これってそのうち引数が大量になってしまうのでは・・

例えば、コンストラクタでDIした場合、今回の例では良いですが、

Sampleが他にも色んなオブジェクトをコンストラクタで渡したいとします。

仮に、引数を3つに増やして見ます。

<?php

interface LoggerInterface
{
public function log($message);
}

class FileLogger implements LoggerInterface
{
private $file;

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

public function log($message)
{
$fp = fopen($this->file, 'a+');
fwrite($fp, $message."\n");
fclose($fp);
}
}

interface SNSManagerInterface
{
public function post($message);
}

class TwitterManager implements SNSManagerInterface
{
public function post($message)
{
// twitterにポストする
}
}

interface UserAuthenticateInterfae
{
public function auth();
}

class DatabaseUserAuthenticator implements UserAuthenticator
{
public function auth()
{
// ユーザ認証を行う
}
}

class Sample
{
public function __construct(LoggerInterface $logger, SNSManagerInterface $sns, UserAuthenticateInterfae $userAuthenticator)
{
$this->logger = $logger;
$this->sns = $sns;
$this->userAuthenticator = $userAuthenticator;
}

public function doSomething()
{
// ユーザ認証して
$this->userAuthenticator->auth();

// SNSで共有して
$this->sns->post('Sampleを始めちゃいました!');

// ログに吐く
$this->logger->log(sprintf('Authenticate user: %s', $user));
}
}

$logger = new FileLogger('/var/log/sample.log');
$twitter = new TwitterManager();
$dbAuthenticator = new DatabaseUserAuthenticator();

$sample = new Sample($logger, $twitter, $dbAuthenticator);

例えば、Sampleを色んな所で使っていた場合を考えてみましょう。

ユーザから、どうやらTwitterにつぶやかれてないみたいだから、Twitterでポストした時にちゃんとログに残して欲しいと言われました。

さぁ、TwitterManagerのインスタンスを生成している(Sampleを使ってる)場所すべてで修正が必要です。

面倒ですよね。


そこでようやくDIコンテナ!

この面倒なインスタンスの生成はすべて別の場所に一括で任せちゃいましょう!

というのを解決するのがDIコンテナです。

ここでは実際にPimpleを使った例を紹介します。

このような各オブジェクトの作り方を知ってるcontainer.phpを作ってみます。


container.php

<?php

require_once __DIR__ . '/vendor/autoload.php';

use Pimple\Container;

use FileLogger;
use TwitterManager;
use DatabaseUserAuthenticator;
use Sample;

$container = new Container();

$container['file.logger'] = $container->protect(function($logFileName) {
return new FileLogger($logFileName);
});

$container['twitter.manager'] = function ($c) {
$logger = $c['file.logger']('twitter_manager.log');
return new TwitterManager($logger);
};

$container['database.authenticator'] = function ($c) {
return new DatabaseUserAuthenticator();
};

$container['sample'] = function ($c) {
return new Sample($c['file.loggger']('sample.log'), $c['twitter.manager'], $c['database.authenticator']);
};


これを使うと、引数地獄だったsampleの生成部分はこのようにスッキリかけます。

require_once __DIR__ . '/container.php';

// 通常にここでインスタンスを生成する場合
// $logger = new FileLogger('/var/log/sample.log');
// $twitter = new TwitterManager();
// $dbAuthenticator = new DatabaseUserAuthenticator();

// $sample = new Sample($logger, $twitter, $dbAuthenticator);

$sample = $container['sample'];

引数の数や内容が変わったとしても、各呼び出しもとを修正する必要がなく、DIコンテナ(container.php)だけ修正すれば良い。という風に出来たのが分かります。


サービスロケータ

さて、DIコンテナを使ってDIを実現しましたが、DIコンテナをよくわからずに使うと、

サービスロケータパターンになってしまうことがあります。

例えば・・

<?php

require_once __DIR__ . '/vendor/autoload.php';

class Sample
{
public function __construct($c)
{
$this->logger = $c['file.logger']('sample.log');
$this->sns = $c['twitter.manager'];
$this->userAuthenticator = $c['user.authenticator'];
}
}

$sample = new Sample($container);

このように、コンストラクタにDIコンテナを渡してしまって、クラスの中でコンテナ内のオブジェクトを探しに行く。という感じですね。

この場合、Sampleクラスは、3つのオブジェクトだけでなく、DIコンテナにも依存することになります。

それ故に、テストする際に毎回コンテナをテストダブルで置き換えて上げる必要が出てきて、若干面倒臭くなります。

また、前のコードではSampleのコンストラクタで必要なクラスが何か(LoggerInterfaceとか、SNSManagerInterfaceとか)分かってましたが、DIコンテナを使う事で、file.loggerに何が入っているかを直接コンテナに見に行くまで分からない。というデメリットも生じます。

サービスロケータが悪。という訳ではないですが、このDIコンテナを使った場合にはサービスロケータパターンにならないようにしておいた方が実装やテストが楽になります。


まとめ

このように、DIもサービスロケータもただのパターンの1つで、DIコンテナはDIとは別物だ。

という事を認識するだけでも大分実装への意識が変わるかもしれません。