依存性注入(DI)の仕組み ― なぜ「外から渡す」だけでコードが劇的に変わるのか


依存性注入(DI)の仕組み DIなし(密結合) ClassA new ClassB() を直接生成 ClassB(固定) ClassC(固定) 差し替え困難 & テスト困難 DIあり(疎結合) DIコンテナ ClassA インターフェースに依存 <<Interface>> 実装B MockB 実装C 自由に差し替え可能!
DIなし(密結合)vs DIあり(疎結合)のイメージ
ひよこ ひよこ
依存性注入って名前が難しそうだけど、どういう仕組みなの?
ペンギン先生 ペンギン先生
すごくシンプルだよ。あるクラスが必要とする部品(依存オブジェクト)を、自分で作らずに外からもらう、ただそれだけの話なんだ。レストランに例えると、料理人が自分で食材を畑から育てるんじゃなくて、仕入れ業者から届けてもらう感じだね。
ひよこ ひよこ
自分で作っちゃダメなの?動くなら同じじゃない?
ペンギン先生 ペンギン先生
動くことは動くんだけど、それだと「密結合」になってしまうんだ。たとえばクラスAの中で直接クラスBをnewしていると、Bを別の実装に差し替えたいときにAのコードを書き換えなきゃいけない。仕入れ業者を変えるたびにレシピを書き直すようなものだよ。
ひよこ ひよこ
それは大変だね…。外から渡す方法にはどんな種類があるの?
ペンギン先生 ペンギン先生
主に3つあるよ。一番よく使われるのがコンストラクタ注入で、オブジェクトを作るときに引数として渡す方法。次にセッター注入で、専用のメソッドで後から渡す。最後にインターフェース注入で、注入用のインターフェースを実装させるやり方だね。現場ではコンストラクタ注入が圧倒的に多いよ。
ひよこ ひよこ
でも依存が多いクラスだと、毎回手動で渡すのは面倒じゃないの?
ペンギン先生 ペンギン先生
そこで登場するのがDIコンテナだよ。SpringやASP.NET Core、Daggerみたいなフレームワークが、どのインターフェースにどの実装を使うかを設定しておくだけで、自動的に依存を組み立ててくれるんだ。いわば配達の手配を全部やってくれるコーディネーターだね。
ひよこ ひよこ
便利だね!DIにするとテストも楽になるって聞いたけど本当?
ペンギン先生 ペンギン先生
本当だよ。依存を外から渡せるということは、テスト時にモック(偽物)を渡せるということなんだ。たとえばデータベースに接続するクラスの代わりに、メモリ上で動くダミーを渡せば、DBなしでテストできる。密結合だとこれができないから、テストのたびに本物のDBを用意しなきゃいけなくなるんだよ。
ひよこ ひよこ
なるほど!逆にDIを使わないパターンでサービスロケータってのがあるみたいだけど、何が違うの?
ペンギン先生 ペンギン先生
サービスロケータは、クラスが自分からレジストリに問い合わせて依存を取りに行くパターンだよ。一見似てるけど、依存が隠れてしまうのが問題なんだ。コンストラクタを見ても何に依存しているか分からないから、アンチパターンとされることが多いね。
ひよこ ひよこ
DIコンテナにもいろんな種類があるみたいだけど、違いはあるの?
ペンギン先生 ペンギン先生
大きな違いはコンパイル時DIとランタイムDIだよ。DaggerAndroid/Java向け)はコンパイル時にコード生成して依存グラフを組み立てるから、起動が速くてエラーも早期に見つかる。一方Springはランタイムにリフレクションで依存を解決するから柔軟だけど、起動が遅くなりがちなんだ。
ひよこ ひよこ
循環依存っていうのも聞いたことあるけど、どういう問題なの?
ペンギン先生 ペンギン先生
AがBに依存して、BがAに依存するケースだね。DIコンテナがどちらを先に作ればいいか分からなくなってエラーになるんだ。Springだとプロキシで遅延解決する裏技もあるけど、そもそも設計を見直すべきサインだよ。責務を分離して中間クラスを挟むのが正攻法だね。
ひよこ ひよこ
DIって万能なの?使いすぎると困ることもある?
ペンギン先生 ペンギン先生
あるよ。コンストラクタ引数が10個も20個もあるクラスは「過剰注入」というコードスメルなんだ。それは1つのクラスが多くのことをやりすぎているサイン。DIは設計の問題を可視化してくれるという意味でも優秀で、引数が多すぎたらクラスを分割するきっかけになるんだよ。制御の反転という原則を意識しつつ、適度に使うのがコツだね。