Dependency Injection(DI)、サービスコンテナ...
知ってるようでしらないことってありませんか?
何番煎じかわからないですが、今回はlaravelの場合に焦点を当てて、それらの言葉の意味やらなんやらを調べてみました。
DI(Dependency Injection)とは?
- Dependency Injection
- DI
- 依存性注入
なんて呼ばれていたりします。
正直、日本語でもいみわかんない。
このままじゃ手も足も出ないので、とりあえず各所でどのように定義されているかを確認してみます。
書籍での定義
"PHP フレームワークLaravel実戦開発, 掌田 津耶乃著"より...
依存関係のあるクラスのインスタンスを外部からクラスに注入するということ
"PHPフレームワーク Laravel Webアプリケーション開発, 竹澤 有貴, 栗生 和明, 新原 雅司, 大村宗太郎 共著"より...
クラスやメソッド内で利用する機能を外部から渡す設計パターンがDI(依存性の注入)です
そして
"Laravel Up & Running A Framework for Building Modern PHP Apps(Second Edition) , Matt Stauffer著"より...
Dependency injection means that, rather than being instantiated("newed up") within a class, each class's dependencies will be injected in from the outside.
さくっと訳してみると...
"依存性の注入は各クラスの依存性をそのクラス内でインスタンス化(newで作成)するのではなく、外部から注入することである。"
こんな感じですか。
Webでの定義
では、次にWeb上からも探してみます。
猿でも分かる! Dependency Injection: 依存性の注入 - Qiitaより、
・依存性
(大雑把に)とあるクラスに、固定の定数、変数、インスタンスが入っちゃっている状態
つまりそのクラスは、その定数、変数、インスタンスに依存している
・注入
そのクラスの外から定数、変数、インスタンスをあるクラスにぶちこむこと
オブジェクトの成立要件に必要な情報を外部設定すること
DI(Dependency Injection)の概要 - Qiitaより、
あるオブジェクトと別のオブジェクトの間の依存関係を薄くするために、依存関係を外から注入するという考え方
などなど...
Webの引用を見る限り、どうやらDIというのはLaravel特有の考え方ではなく、もっと大きな範囲の(言語やフレームワークに縛られない)設計手法のようです。
DIの定義
どれも似通っている感じですが、特に書籍三冊を比べると、定義の形が似ていますね。
共通部分を抜き出すと...
"クラス等へ(インスタンス/機能/依存性)を外部から(注入する/渡す)こと。"
こんな感じでしょうか。
これだけだとよくわかんなくてもやもやが残りますね...
そのもやもや、覚えておいてください。
このもやもやをすっきりさせるためには、サービスコンテナについても知っておく必要があるので、サービスコンテナについて語ったのち、改めて話題に出そうと思います。
サービスコンテナとは?
また出ました。
よくわからん単語。
再度書籍からそれらしい場所探してみます。
どうでもいいですが、最近コンテナ(洋画の貨物船にめっちゃ乗ってるアレ)の中に家具置いて生活するコンテナハウスが流行ってるって聞きました。
...。
安いなら検討ありかも...。
サービスコンテナの定義
"PHP フレームワークLaravel実戦開発, 掌田 津耶乃著"
あるクラスと依存関係にあるクラスのインスタンスを管理する機能を提供する
また、同著前述箇所直後にて、こうとも述べられていました。
サービスコンテナとは、Laravelに用意されているDI機能を実装したクラスである
"PHPフレームワーク Laravel Webアプリケーション開発, 竹澤 有貴, 栗生 和明, 新原 雅司, 大村宗太郎 共著"
Laravelフレームワーク内におけるインスタンス管理の役割を担っている
"Laravel Up & Running A Framework for Building Modern PHP Apps(Second Edition) , Matt Stauffer著"より...
The container is a simple tool you can use to bind and resolve concrete instances of classes and interfaces, and at the same time it's a powerful and nuanced manager of a network of interrelated dependencies.
簡単に訳してみると...
コンテナはクラスやインターフェースの具体的なインスタンスをバインド、解決するために使えるシンプルなツールです。さらに、相互の依存関係の繋がりを繊細、かつ力強く制御することもできます。
こんなところでしょうか....
ちなみに、Laravel Up & Runningにて、サービスコンテナの表記が"container"となっていますが、
サービスコンテナと同義と考えていただいて大丈夫です。
併せて、以下も全て同一のものを指しますよ〜と、述べられています。
- Application container
- IoC(inversion of control) container
- Service container
- DI(dependency injection) container
では、これらの定義をなんとな〜くまとめると...
"サービスコンテナはインスタンスを(バインド, 解決)することができ、その依存関係も制御できる物、つまり、インスタンスを管理できる物"
こんな感じかな。
バインド、解決に関してはのちに改めて解説します。
その前に、サービスコンテナとDIの全体像を掴むため、ちょっとした例をみてみましょう。
DIとサービスコンテナのコード例
定義と睨めっこするのも飽きてきたので、休みがてらコードをみてみましょう。
class HogeController extends Controller
{
public function index(HogeService $hogeservice) // メソッドの引数でHogeServiceクラスを指定
{
$hello = $hogeservice->hello(); // Hello World!!! (HogeServiceクラスのインスタンスを使用できる)
// ....
}
}
class HogeService
{
private $msg;
public function __construct()
{
$this->msg = 'Hello World!!!';
}
public function hello()
{
return $this->msg;
}
}
十分休めました?
なんてことのない、indexメソッドでHello World!!!という文字列を$hello
に代入するプログラムです。
皆さんはすでにご存知かもしれませんが、Laravelではコントローラーのコンストラクタやメソッドにて、引数と型宣言的なものをつけると、宣言したクラスのインスタンスを使えます。
これって、別のクラス(HogeServiceクラス)のインスタンスをHogeControllerクラスのindexメソッド内で$hogeservice
として使ってますよね。
つまり...?
"あるクラス(HogeController)のメソッド(index)の引数として、外部から別のクラス(HogeService)の機能(HogeServiceのインスタンス)を渡している"
ここでDIの定義もう一回みてみます。
"クラス等へ(インスタンス/機能/依存性)を外部から(注入する/渡す)こと"
なんとな〜く見えてきた気がしませんか...?
一緒やん!
つまり、引数宣言で勝手にインスタンスを使えちゃうあれ、DIなんですね。
ではなぜ引数に宣言しただけでインスタンスが使えるようになるのか...?
それを紐解く上でサービスコンテナが大活躍します。
端的に言うと...
”サービスコンテナがバインドに従って解決をすることで、インスタンスを$hogeservice
に渡してくれます。”
...
急にややこしくなりましたし、バインドと解決また出てきましたね。
詳しく見ていきましょう。
バインド(結合)とソリューション(解決)
サービスコンテナがやっていることは物の貸し借りの管理に似ている気がします。
サービスコンテナに借りたいものを伝えると、サービスコンテナは台帳をみて、それに従って適切なものを適切な処理を加えた上で貸してくれるわけです。
その台帳を作る作業がバインド(結合)で、借りたい!と伝えて、実際に貸してくれるまでの一連の動作をソリューション(解決)といいます。
そして、ものを貸す側と、借りる側の間を取り持ってくれているのがサービスコンテナです。
もうちょっと実践的にいうならば...
サービスコンテナにバインド(クラス名の指定に対してどのクラスのインスタンスを作るのか、どのようにインスタンスを作るのかの定義を)してあげます。
そして、別のクラスのインスタンスを使いたくなったら、先ほどのようにメソッドの引数に使いたいクラスのクラス名を型宣言してあげます。
すると、引数に指定されたクラス名から、バインドで指定したクラス名を参照して、欲しいクラスのインスタンスを生成し、もともと定義していた変数に値を渡してくれます。つまり、"解決"してくれるわけです。
- バインド(結合) :サービスコンテナに名前とインスタンスの生成方法を定義すること
- ソリューション(解決):クラス名が指定されたら、そのインスタンスをサービスコンテナが生成して返すこと。
これで、さっきのよくわからない文章も完璧ですね。
"サービスコンテナがバインドに従って解決をすることで、インスタンスを$hogeserviceに渡してくれます。"
という一文は、
"サービスコンテナがクラス名とインスタンスの生成方法の定義に従って指定されたクラスのインスタンスを返すことで、そのインスタンスを$hogeserviceに渡してくれます。"
とすると読みやすくなりますね。
だがしかーし!
”DIとサービスコンテナってのがなんなのかはわかった!でも、$hogeservice = new HogeService();
"すればおんなじようにインスタンス使えるじゃん?それじゃだめなの?”
という声が聞こえてきますね。
さすがです。
newを使ったらダメなの?
PHPにはインスタンスを作る方法の1つとしてnewがありますね。
クラス名を含む文字列を new で指定すると、 そのクラスのインスタンスを作成します。
newを使用することでインスタンスをクラス内に生成することができます。
しかしながら、このnewを使うことで困ってしまうシーンが出てきます。
具体的なシーンをお伝えする前に、new, DIでインスタンスを使う場合の違いについて軽く触れておきます。
newとDIでのインスタンス生成過程の違い
先ほどのnewの定義にあったように、newをした場合、"あるクラスの中で(新しい)インスタンスを作っている"(=内部で新しく作られたインスタンスを使う)ので、先のDIの定義である"外部から渡す"(=外部で作られたインスタンスを使う)部分が足りないんです。
一見どちらもあるクラスの中で別クラスのインスタンスを使えて、同じように見えますが、自社製のインスタンスか、他社製のインスタンスかが決定的な差になってきます。
"外部から渡す"ことができれば、文字通り外部で作成したインスタンスを渡して活用することができます。
一方、newされたら、毎回クラス内で新しいインスタンスが生成されるわけです。
newだと困ること
さて、違いはわかりましたね。
では、一体何が困るんでしょうか...?
ざっと調べてみたところ、以下が挙げられそうです。
- あるインスタンスの情報を別のクラスで使いたい時
- シングルトンを使いたい時
- 単体テストが楽にしたい時
- 開発を楽にしたい時
- 保守運用を楽にしたい時
あるインスタンスの情報を別のクラスで使いたい時
インスタンスに情報を保持し、その保持した情報をそのまま別の場所でも使いたい場合などかこれにあたるでしょう。例えば、環境設定の値管理やキャッシュ保持などはシステム全体を通してインスタンスを1つだけ生成すればいい場合が多いです。そんな時にあるインスタンスをデータはそのままで渡したりもらったりすることができたら嬉しいですよね。
共通のインスタンスを使わずにビジネスロジックで対処することもできるかもしれませんが、コードの複雑化に繋がってしまうため、避けたいところです。
シングルトンを使いたい時
シングルトンといった実装方法(たった1つのインスタンスしか生成できないクラス等)を用いた場合、予め用意されているインスタンスを外部から取得する必要(シングルトンのインスタンスを外部で使いたい場合にnewできない)があります。
シングルトンについては省略...
単体テストが楽にしたい時
クラスAの単体テストを行いたいとしましょう。
クラスAのメソッド中でBクラスのインスタンスをnewする場合...
クラスBの改修によってクラスAのテストが失敗してしまう可能性があります。
つまり、クラスAはクラスBに依存しているわけです。
だから、その依存している箇所を外から渡すようにDIで設計してあげることで...?
クラスAに渡すクラスBのインスタンスを偽造(正しい値を想定したインスタンスに置き換え)することができ、たとえクラスBの実装を変更してもクラスAのテストには影響が無くなります。
これに関してはほとんど以下の記事の受け売りです。
是非一読してみてください!(とてつもなくわかりやすいです!
開発を楽にしたい時
例えばクラスAとBを実装しようとした場合、クラスAがBに依存していたら、クラスAの実装はクラスBの実装が終わった段階でしか取りかかれません。先ほどのテストの例から、DIでは仮にクラスBが出来ていなくても、クラスAの開発に着手することができます。
つまり、たとえクラスが依存していても、未完成のクラスの正しい挙動が分かっていれば、それに合わせてインスタンスを偽装し、テストを進めながら開発ができるというわけです。
保守運用を楽にしたい時
例えばあるクラスのインスタンスをnewする場合は、直後にinit(初期化)したい!ってなったとします。
newで実装している場合、newでインスタンスを作成した箇所全てに対して、作成したインスタンスからinitメソッドを呼び出す作業を書いてあげる必要があります。
これはDRYでないですし、変更が入るたびに全箇所修正するとなると骨が折れます。
いやですよね。
しかし、DIだと、バインドの定義時にちゃんと定義してしまえばあとは勝手にやってくれるので、そういった共通処理をちょっと挟みたい!といった場合にも、改めて定義を一箇所だけ修正してあげれば呼び出し元は修正しなくても大丈夫です。
さらに、interfaceを用いた実装をすることによって、使いたいクラスの切り替えも容易になります。
laravelではありませんが、概念に関してとてもわかりやすいので参考にしてみてください。
猿でも分かる! Dependency Injection: 依存性の注入 - Qiita
Laravelでのinterfaceの活用について詳しく知りたい方は、今回紹介した書籍全てに書かれているので、是非読んでみてください。(Up & Runnningは洋書しかないみたいです)
こういった理由もあり、newを使うのはできるだけ避けたいよね〜といった感じのようです。
巷でもよくDIで変更に強くなる!テストしやすくなる!と言われていますが...こういうことだったのね。
まとめ
DIが何なのか、ほんのちょっぴり分かった気がします。
社内の資料でも割とよく見かけるDI、これからはもうちょっと理解した上で読めそうです。
今回、長くなるので省略してしまいましたが、まだまだDIの氷山の一角です。
実はDI、引数宣言以外の手法もあったりしますし、実際のユースケースに近い活用法周り等はまだまだ調べきれていません...
ちなみに、今回説明に使った手法はindexメソッドに注入(型宣言でインスタンス代入)していたのでメソッドインジェクションと呼ばれていたりします。
今度改めてDIの沼に頭まで浸かっておかないとなぁ...。
長々と読んでいただきありがとうございました。
誤字脱字、ここはこうじゃない?等のフィードバックはいつでも募集中です!