PHPの現場 にて、DI 談義を行うので、頭を整理しておくためのメモです。
DI についてきちんと知りたいのであれば、参照に挙げたリンク先に有用な記事があるので、そちらを参考にして下さい。
PHP を念頭に置いてますが、Java など他言語でも大枠は同じだと思います。この内容は、いずれ整理するかもしれませんし、そのままかもしれません。
DI という言葉
「DI」が差す意味合いが、依存オブジェクトの注入だけなのか、DI コンテナによる注入を含んでいるのか、DIP まで意識しているのかが、人やコンテキストによって違っていそうで、そこを揃えてから議論しないと。
— Masashi Shinbara (@shin1x1) May 19, 2017
DI について話す時に、何を差すのかが異なると話が噛み合わない。そこで、それぞれに名前を付ける。
- DI パターン = 依存オブジェクトを注入することを差す。DI コンテナを利用するかどうかは問わない。(GoF の Strategy パターン)
- DI コンテナ = DI パターンを実現するために便利なライブラリ。依存オブジェクトの生成、注入、ライフサイクル管理*1を行う。フレームワークに内包されていることもある。
DIパターンの適用範囲は広く、PHP でも型宣言(タイプヒント)を用いて型を指定する場合はもちろんのこと、型宣言が無い場合や無名関数を与える場合も DI の一種と言える。
実際、AngularJS では、型を指定しない(TypeScript など使えば型指定可能)が、DI を利用している。
- 「DI = DI コンテナの利用」ではない。DI コンテナを使わなくても、DI パターンの実現は可能。
- 型指定が無くても、DI できる。
DI パターン
DI パターンでは、依存オブジェクトを型で指定するのが一般的だ。型の指定は、下記の順で、より結合度が低くなり、依存が明確になる。
- 型指定無し
- 具象クラス
- 抽象クラス
- インターフェイス
このあたりは、DI に限らず、インターフェイスをなぜ利用するのか?具象クラスだけで良いのでは?という議論にも発展できる。
DI パターンのメリット
- クラス間を疎結合にする。(結合度を下げる。)
- それぞれが独立したモジュールになり、自身の役割のみに専念できる。(凝集度が高くなる。)
- クラスの役割が明確で、限られたものになるので、実装しやすく、テストもしやすく、利用しやすくなる。(脳に優しい)
- 必要な依存が明確になる。
- インターフェイスに依存した場合、インターフェイスの API のみが依存箇所なので、それ以外の部分(API の実装や与えられたインスタンスが持つ他のメソッドなど)は一切気にする必要がなくなる。(脳に優しい)
DI パターンのデメリット
- ファイルが増える。
- 実装を追いにくくなる。(結局は実装読まないと分からないでしょ)
- インターフェイスに依存とか言ってるけど、PHP だと与えられたインスタンスのメソッドは何でも呼べてしまう。(IDEでは警告は出るが、実行はできる)
依存オブジェクトの注入
依存オブジェクトを注入するには、下記のような方法がある。
- コンストラクタインジェクション
- セッターインジェクション(インターフェイスインジェクション)
- フィールドインジェクション
- メソッドインジェクション
どの方法でも実現はできるが、コンストラクタインジェクションが推奨されている。
- インスタンス生成のシーケンスで依存オブジェクトを注入できる。特殊な操作が不要。
- インスタンスが生成されたときには、依存オブジェクトは注入済なので、安全に利用できる。
- コンストラクタでのみ注入できるので、インスタンスにどのような操作を行っても、外部から依存オブジェクトを差し替えられて、想定外の動きをするなどを防ぐことができる。
このあたりの利点は、完全コンストラクタとも似ている。
DI コンテナ
依存オブジェクトをアプリケーション自ら与えることで、DI パターンは実現できるが、実際のところ、DI コンテナを利用するのが一般的だ。
PHPには、数多くの DI コンテナがある。Web アプリケーションフレームワークを利用している場合、内包されていることが多く、意識せずに利用しているかもしれない。
PHP の主な DI コンテナは以下。
- Pimple - A simple PHP Dependency Injection Container
- illuminate/container: [READ ONLY] Subtree split of the Illuminate Container component (see laravel/framework)
- Aura.Di/index.md at 3.x · auraphp/Aura.Di
- ray-di/Ray.Di: Guice style dependency injection framework for PHP
- zend-servicemanager
- zend-di
- PHP-DI - The Dependency Injection Container for humans
PHP では、PSR-11 にて DI コンテナの API が規定されている。
fig-standards/PSR-11-container.md at master · php-fig/fig-standards
ワイヤリング
DI コンテナは、依存オブジェクトの生成(取得)方法を知っておく必要がある。これには、ファクトリ(クラスや無名関数等)や設定ファイル(YAML や XML)を利用する。
ただ、全ての生成方法を指定していくのは骨が折れる作業なので、あらかじめ決められたルールや最低限の設定で自動でオブジェクトを生成する auto-wired(Auto-Wiring, Auto-Wire) という仕組みがある。
例えば、Laravel の DI コンテナでは、型宣言に具象クラスを指定すれば、ワイヤリングの設定が無くとも、new でインスタンスを生成する。また、インターフェイスの場合は、対応する具象クラスを設定しておけば、そのインスタンスを生成する。
DI コンテナとサービスロケータ
DI コンテナと比較して良く登場するのが、サービスロケータだ。これは、オブジェクトコンテナを依存として注入し、利用時に必要な依存オブジェクトをこのコンテナから取り出すというパターンである。
サービスロケータは、DI を使わずに依存オブジェクトを内部で生成するパターンの発展系で、一方、DI コンテナは、外部から依存オブジェクトを与えるので、制御の方向が逆とも言える。
サービスロケータでも、DI コンテナと同様に、実装の差し替えや依存オブジェクトの生成やライフサイクルの管理といったメリットを享受できる。
ただ、DI パターンのメリットである、依存を明確にする、最小限にするという効果は薄れる。
このあたりは、何を求めて利用するのかという目的によって使い分けると良い。(サービスロケータが全てダメとかいう短絡的な話ではない)
PHP で DI が必要か?
PHPでDI流行ってるの、便利なんだけどもやもやはしている。もっと密結合でいいじゃん、という。
— Hiroyuki Yamaoka 🐰 (@hiro_y) May 18, 2017
収録分を公開しました。
7. PHP に DI は必要か?(hiro_y / ytake) | PHPの現場
参照
- Inversion of Control コンテナと Dependency Injection パターン
- やはりあなた方のDependency Injectionはまちがっている。 — A Day in Serenity (Reloaded) — PHP, FuelPHP, Linux or something
- PHP Mentors -> Pimpleでシンプルに正しくDIを理解する
- Advent Day 10: DI > SL ? « BEAR Blog
- PHP: The Right Way
*1:HTTP リクエスト単位で全てを破棄する PHP では、オブジェクトのライフサイクル管理の重要性は他言語とは異なる。