やはりあなた方のDependency Injectionはまちがっている。
今日はPHP界隈で大人気のDependency Injectionと、それに関連する用語について整理しておこうと思います。
以下のような状況があるのではないか?と思ったからです。
- 多くのPHPユーザがDependency Injection(DI)をよくわかっていない、あるいは正確に説明できません。
- そして、デザインパターンである「DIパターン」とDIをサポートするツールである「DIコンテナ」を混同しています。
- また、「DIパターン」と「サービスロケータパターン」をうまく区別できていません。
Dependency Injectionとは何か?
Dependency Injectionとは「Dependency」を「Injection」するというデザインパターンです。
日本語では何故か「依存性の注入」と訳されており、これが混乱の元ではないかと思います。
日本語で「依存性」と言うと、「依存性はコカイン並み」「ニコチンは依存性薬物」のような用法がほとんどで、「依存する性質」というような意味になります。
そのような「依存性」を「注入する」と言うと、もうかなり意味不明ですね。
依存性の注入ってよく考えたらすごい字面だ・・依存性のあるモノを注入するとか・・非合法の香りしかしない
— ngyuki(えぬじーぶるー) (@ngyuki) June 22, 2015
たぶん、多くの人は「依存性」を「依存関係」のように理解しているのではないかと思います。「依存関係を注入する」だと少しましな気はします。
ただ、やはり日本語としては意味不明で、また、そのような理解が誤解の一因になっている気がします。
「注入する」という場合、もっと具体的な物が必要です。
「Dependency」は「依存性」ではない
それでは、Wikipdediaの説明を見てみましょう。
A dependency is an object that can be used (a service). An injection is the passing of a dependency to a dependent object (a client) that would use it. The service is made part of the client's state.[1] Passing the service to the client, rather than allowing a client to build or find the service, is the fundamental requirement of the pattern.
https://en.wikipedia.org/wiki/Dependency_injection
ここでは、明確に「dependency」とは「オブジェクト」であると定義されています。
「オブジェクトを注入する」と言うと日本語としても意味がはっきりしていますね。
Wikipediaによると「dependency」は使われるオブジェクト(サービスと呼ぶ)であり、「injection」とはそのオブジェクト(=サービス)を、それを使うオブジェクト(クライアントと呼ぶ)に渡すことです。
ちょっとわかりにくいですが、簡単に言うと、あるオブジェクト(=サービス)を別のオブジェクト(=クライアント)に渡すパターンがDIパターンです。
ちなみに、「サービス」と「クライアント」という用語は、関係性を表現しているだけです。使う方を「クライアント(依頼者)」、使われる方が「サービス」になります。
Wikipediaだけでは信用できないという人もいるかも知れませんので、他にも調べてみましょう。
マイクロソフトのランゲージポータルには、「dependency」について以下の説明があります。
For example, if feature A depends on feature B, B is a dependency of A. https://www.microsoft.com/Language/ja-jp/Search.aspx?sString=dependency&langID=ja-jp
「機能Aが機能Bに依存している場合、BがAのdependencyである」と記載されています。
また、Dependency Injectionという用語を作成したと言われるマーティン・ファウラーの文章の翻訳でも、「dependency」は「依存オブジェクト」と訳されています。
結論をいえば、このパターンにはもっと明確な名前が必要なように思う。 「制御の反転」という用語では包括的すぎる。これでは混乱する人が出てくるのも無理はない。 様ざまな IoC 支持者と多くの議論を重ねた末、その名前は Dependency Injection (依存オブジェクト注入)に落ち着いた。
http://kakutani.com/trans/fowler/injection.html
以上のように、「Dependency」は「依存性」ではなく「使われる側のオブジェクト」(依存オブジェクト)という意味になります。
DIパターンのコード例
コードを見た方がわかりやすいかも知れません。以下のようなパターンがDIです。
<?php
class Client
{
private $service;
public function __construct(Service $service)
{
$this->service = $service;
}
public function doSomething()
{
$this->service->doSomething();
}
...
}
class Service
{
public function doSomething()
{
...
}
...
}
Clientクラスがクライアント、Serviceクラスがサービス(=dependency)です。
DIはデザインパターンなので、あるオブジェクトが外部から渡されていればそれはDIパターンです。
上記のコードではコンストラクタを使ってサービスを注入してるため、「コンストラクタインジェクション」と呼ばれます。
他にも、注入するためのメソッドを用意する「セッターインジェクション」や、プロパティに直接注入する「プロパティインジェクション」という方法もあります。
で、DIって何なの?
デザインパターンです。
DIパターンでないコード例
先ほどのコードをDIを使わないように書き換えると、以下のようになります。
<?php
class Client
{
private $service;
public function __construct()
{
$this->service = new Service();
}
public function doSomething()
{
$this->service->doSomething();
}
...
}
class Service
{
public function doSomething()
{
...
}
...
}
たぶん、この方が馴染みのあるコードかも知れません。
この場合、Clientクラス内でServiceオブジェクトが生成(new
)されています。
一方、DIパターンの方は、Serviceオブジェクトは外部で生成され、Clientクラスに注入されることになります。
DIパターンの特徴
DIパターンでは、
- オブジェクトの生成と使用が分離されている
- クライアントがサービスを呼ぶのではなく、サービスが外部からクライアントに注入される。つまり、制御が反転している
と言えます。
例えば、ServiceがPDOオブジェクトだとして、DIパターンの場合は、Clientクラスのコードを一切変更せずに、
- あるClientオブジェクトにはmasterに接続するPDOオブジェクトを
- 別のClientオブジェクトにはslaveに接続するPDOオブジェクトを
注入することができます。
依存とは何か?
ここで、「依存」あるいは「依存している」という言葉について整理しておきます。
「オブジェクトAがオブジェクトBに依存している」とは、「オブジェクトAが内部でオブジェクトBを使用している」ということです。
以下のコード(先ほどのDIパターンでないコード例)の場合、ClientはServiceに依存しています。
<?php
class Client
{
private $service;
public function __construct()
{
$this->service = new Service();
}
public function doSomething()
{
$this->service->doSomething();
}
...
}
class Service
{
public function doSomething()
{
...
}
...
}
ClientオブジェクトはServiceオブジェクトがないと動作できません。これが「依存している」ということです。
DIパターンの場合でも、やはりClientはServiceに依存しています。ClientオブジェクトはServiceオブジェクトがないと動作できないからです。
DIパターンを使ったからと言って、依存がなくなるわけではありません。
抽象に依存せよ
ただし、DIパターンを使った場合は、以下のようなコーディングも可能になります。
<?php
class Client
{
private $service;
public function __construct(ServiceInterface $service)
{
$this->service = $service;
}
public function doSomething()
{
$this->service->doSomething();
}
...
}
interface ServiceInterface
{
public function doSomething();
...
}
class Service implements ServiceInterface
{
public function doSomething()
{
...
}
...
}
この場合、ClientはServiceInterfaceインターフェイスを実装したオブジェクトがあれば動作できます。
Serviceオブジェクトでなくても別のServiceInterfaceインターフェイスを実装したオブジェクトがあればいいということになります。
このような場合は、「抽象に依存している」と言います。
Clientは、具象(コンクリート)クラスには依存していませんが、インターフェイスを実装したクラスに依存しています。
抽象に依存するようにすれば、サービスをDIで別の実装(クラス)に差し替えることができ、クライアントクラスとサービスクラスの結合度が緩くなります(疎結合)。
逆に、DIパターンでないコード例のようなクラス内でnew
しているコードの場合、クライアントクラスとサービスクラスの結合度がきついです(密結合)。
DIコンテナとは?
DIパターンについて上記で解説しました。
このパターンは「使われるオブジェクト(=サービス)」を外部からクライアントに注入するというパターンなので、クライアントクラスの外部でサービスオブジェクトを生成し注入すれば、それはDIパターンを使っている(=DIしている)と言えます。
Clientクラスを使うコードは、以下のようになります。これを「手動でのDI」と言います。
$service = new Service();
$client = new Client($service);
この程度のコードならこれで何の困難もありません。しかし、オブジェクトがたくさんあり依存関係が複雑になると、オブジェクト生成のコードも複雑になり大変になります。
そこで、DIを楽にしたり、オブジェクト生成のコードをまとめるための便利なツールとして「DIコンテナ」が登場します。
シンプルなDIコンテナである Pimple を使うと、コードは以下のようになります。
use Pimple\Container;
$container = new Container();
// オブジェクト生成の設定
$container['service_interface'] = function ($container) {
$service = new Service();
return $service;
};
$container['client'] = function ($container) {
$client = new Client($container['service_interface']);
return $client;
};
// オブジェクトの使用
$client = $container['client'];
これが、DIコンテナを使ったDIです。
元のコードがシンプル過ぎるので、単にコードが増えて面倒になっただけに感じるかも知れませんが、もしクラスが増えてくると便利になる気がしませんか?
もし便利になる気がしないなら、あなたのプロジェクトにはDIコンテナはまだ必要ないということでしょう。無理にDIコンテナを使う必要はありません。DIコンテナがなくてもDIは使えます。
サービスロケータとは何か?
ところで、DIコンテナを使えば自動的にDIパターンになるかというと、答えはNoです。
なぜなら、DIコンテナを使うと以下のようなコードも書けるからです。
use Pimple\Container;
class Client
{
private $container;
public function __construct(Container $container)
{
$this->container = $container;
}
public function doSomething()
{
$this->container['service_interface']->doSomething();
}
...
}
interface ServiceInterface
{
public function doSomething();
...
}
class Service implements ServiceInterface
{
public function doSomething()
{
...
}
...
}
Clientクラスを使うコードは、以下のようになります。
use Pimple\Container;
$container = new Container();
// オブジェクト生成の設定
$container['service_interface'] = function ($container) {
$service = new Service();
return $service;
};
$container['client'] = function ($container) {
$client = new Client($container);
return $client;
};
// オブジェクトの使用
$client = $container['client'];
大きな違いは、Clientクラスがコンテナに依存していることです。
「サービスロケータ」とは、サービス(オブジェクト)の取得を抽象化するデザインパターンです。あるアプリケーションが必要とするサービスのすべてを保持するレジストリ(サービスロケータ)を用意します。
上記のコードでの$container
がまさにサービスロケータです。
サービスロケータはアンチパターンである
PHP: The Right Way はサービスロケータについて以下のように説明しています。
DIコンテナは、依存性の注入を実現するための便利な道具として使える。 でも、使い方をミスって、サービスロケーションというアンチパターンを作ってしまっていることも多い。 DIコンテナをサービスロケーターとしてクラスに組み込んでしまうと、 依存関係を別の場所に移そうとしていたはずなのに、よりきつい依存関係を作り込むことになる。 おまけにそのコードはわかりにくくなってしまうし、テストもしづらくなる。
なんだかサービスロケータはいいことなしのダメパターンのような言われ方ですね。
サービスロケータの何が悪いのか?
以下のDIの場合のコードと比べてサービスロケータの場合、何がそんなに悪いのでしょうか?
DIの場合
class Client
{
private $service;
public function __construct(ServiceInterface $service)
{
$this->service = $service;
}
public function doSomething()
{
$this->service->doSomething();
}
...
}
サービスロケータの場合
use Pimple\Container;
class Client
{
private $container;
public function __construct(Container $container)
{
$this->container = $container;
}
public function doSomething()
{
$this->container['service_interface']->doSomething();
}
...
}
まず、DIの場合、Clientクラスが依存しているのはServiceInterface
(を実装したオブジェクト)のみです。
サービスロケータの場合は、コンテナ(Pimple)と$this->container['service_interface']
に依存しています。
依存するオブジェクトが増える
つまり、サービスロケータの場合、依存するオブジェクトが1つ増えています。
この場合のClientクラスはコンテナがないと動作できません。しかし、本来Clientクラスが必要なのはServiceInterface
(を実装したオブジェクト)のみです。
仮にClientクラスがあるライブラリだとすると、そのライブラリはコンテナ(この場合はPimple)と一緒でないと使えません。例えば、自分の使っているフレームワークが別の(Pimpleとインターフェイス互換でない)コンテナを使っていると、そのライブラリを使うためだけにPimpleをインストールする必要が生じます。
このようにサービスロケータを使うと、本来必要ないコンテナへの依存が発生してしまいます。
テストが面倒になる
このクラスをテストするには、コンテナが必ず必要です。コンテナをテストダブルで置き換えることもできますが、DIの場合ならそもそもコンテナは不要です。このようにサービスロケータを使うとテストが少し面倒になります。
依存するオブジェクトが何かわからない
それから、サービスロケータの場合で依存している$this->container['service_interface']
とは何でしょうか?
これはコンテナでの定義を調べないとわかりません。つまり、コードが読みにくくなっています。
一方、DIの場合なら、Clientクラスのコードを見ただけで、ServiceInterface
インターフェイスに依存していることがすぐにわかります。
さらに、コンテナにはすべてのサービスが登録されているため、どんなサービスでもいくらでもそこから取得できます。本来依存すべきでないサービスに依存するコードも簡単に書けますし、そのクラスが何に依存しているかもわかりづらいコードになります。
上記のような理由から、「PHP: The Right Way」ではサービスロケータをアンチパターンである、としています。
DIとサービスロケータの区別
サービスロケータでもサービスの差し替えは可能
以上のようなデメリットのあるサービスロケータですが、コンテナでの定義を変更することでサービスオブジェクトを差し替えることができる点は、DIと変わりません。
テスト時にサービスを簡単にテストダブルに差し替えることもできます。
つまり、クライアントのコードを一切変更することなしに、クライアントが使用するオブジェクトを差し替えることで振る舞いを変えることができます。
ということで両者は似ており、区別しづらい面があることも確かです。
DIとサービスロケータの区別の方法
最後に Quicker, Easier, More Seductive: Names, Usage, and Intent | Paul M. Jones より、DIとサービスロケータの区別の方法を紹介しておきます。
- コンテナをファクトリでないオブジェクトの 外側 で使う場合、あなたはコンテナをDIのために使っている。
- コンテナをファクトリでないオブジェクトの 内側 で使う場合、あなたはコンテナをサービスロケータとして使っている。
まとめ
- DIはデザインパターンであり、ツールであるDIコンテナとは別の概念です。
- Dependency InjectionのDependencyは依存性ではありません。「使われるオブジェクト」(依存オブジェクト)という意味です。
- DIコンテナを使っていれば、必ずDIパターンになるわけではありません。
- DIとサービスロケータは別のデザインパターンです。
- サービスロケータはアンチパターンです。
参考
Date: 2015/08/31