ぼやっとは理解してたけど、この記事を見て知識が定着した気がする。
DI・DIコンテナ、ちゃんと理解出来てる・・? - Qiita
この記事ではDIとDIコンテナとサービスロケータについて触れてたけど、
ここではDIについてだけ改めて自分でまとめてみる。
サンプルケース
例えばこんなコードがある。
※phpぽいけど疑似コードです。
class Human { public void function __construct(string name) { this->brain = new Database() # DBアクセスライブラリ this->name = name } # 覚える public void function memolize(string keyword, string some_string){ this->brain.save(keyword, some_string) } # 思い出す public void function remember(string keyword){ return this->brain.load(keyword) } # 名前を取得する public string function getName(){ return this->name } }
人がキーワードで文字列を記憶する。 こんな感じで使う。
Human human = new Human("太郎") human.memolize("好きなおかず", "おでん") human.remember("好きなおかず") # "おでん" human.getName() # 太郎
これは内部でDBアクセスライブラリをnewしている。これはDIしてない例。
なにが悪いのか?
このHumanクラスはDBアクセスライブラリに依存している。そうなるともちろんDBに依存している。
「依存している」というだけで悪い理由としては充分だけど、具体的に悪い理由を記述してみる。
開発でちょっとその部分だけ動かしたくてもDBをセッティングしないと動かない
DBの設定がうまく出来ていないとnew Database()の部分でエラーが発生してHumanクラスは動かない。
例えばHumanクラスのgetName()メソッドの挙動を見たい時、このメソッドはDB全く関係ないのにDBの設定が必要になってしまう。
開発でなんかデータが入っている状態を再現してコードを実行してみたい場合、DBに実際にデータを入れないといけない
「DBからこの値が返ってきた時」の動きを見たい時、実際にDBにそういう値が返ってくるようにデータを入れないといけない。
簡単に入ればいいけど実際には「このデータを入れるには親テーブルにデータが入っていないといけない」など、整合性が取れたデータを入れる必要があったりして、すんなりと用意できない場合がある。
ちょっとしたシンプルなテストデータを返してもらうために、壮大な疑似データを作成していたりすると、「俺はなにをやっているんだ」という気持ちになる。
こういうのが大変だからという理由で「テーブルにリレーションを貼らない」みたいな運用をするのは本末転倒だと思う。
テストコード実行がDBをセッティングしないと動かない
DBの設定をしないとHumanクラスが動かせないから、もちろんテストコード実行もDBの設定が必要になる。
実際にDBに読み書きするのでテスト実行が遅くなるし(並列実行も難しい)、DBへのテスト実行前準備処理、テスト実行後の掃除処理なども必要になってテストが複雑になる。
テストコード側もDBに依存する
テストの成功失敗判断は意図したデータがDBに入っていることを確認して行う必要があるので、DBアクセスする必要がある。だからテストコード側もDBに依存することになる。
仮に、機能変更でHumanクラスのデータ保存先がDBではなくなった場合、テストコード側にも大量の修正が必要になる。
DBライブラリを変えたくなった場合に大変
メンテされなくなったとかいう理由でDBライブラリを変更したくなった時、Humanクラス側を触って入れ替えないといけない。
大量に使われていた場合、すべての挙動が変わらないことを担保するのが大変になる。
特にライブラリ固有の凝った便利メソッドとか使っているともう交換不可能みたいな感じになったりする。
※ちゃんと全部テスト書かれていたらまだいいけど。
場合によっては保存先を変えたい場合
保存先をDBだけではなくて、textにしたかったり、どこかのhostにapi送信にしたかったりした場合に簡単にはできない。複雑なif条件分岐が入り込みそう。
DIにした場合
こんなコードになる。
# インターフェース定義 interface Brain{ public void save(string keyword, string some_string) public string load(string keyword) } # Brainインターフェースを実装したクラス。DBアクセスライブラリを使ってDBにデータを保存する。 class DbBrain implements Brain{ public void function __construct(){ this->db = new Database() } public void save(string keyword, string some_string){ this->db.save(keyword, some_string) } public string load(string keyword){ return this->db.load(keyword) } } class Human { public void function __construct(string name, Brain brain) # Brainインターフェースのオブジェクトを受け取る。 { this->brain = brain this->name = name } public void function memolize(string keyword, string some_string){ this->brain.save(keyword, some_string) } public void function remember(string keyword){ return this->brain.load(keyword) } public string function getName(){ return this->name } }
こんな感じで使う。
Brain db_brain = new DbBrain() Human human = new Human("太郎", db_brain) # ここがDI human.memolize("好きなおかず", "おでん") human.remember("好きなおかず") # "おでん" human.getName() # 太郎
これはHumanクラスの外部からdb_brainを渡している。ここがDIしている部分。
今回はコンストラクタで渡したので「コンストラクトインジェクション」と呼ばれるらしい。
(他にもセッタで渡すセッタインジェクションとかあるらしい。)
なにが良いのか?
このHumanクラスはBrainインターフェースに依存しているだけで、DBアクセスライブラリに依存していない。もちろんDBにも依存していない。
インターフェースは実装の無い単なる型なので、具体的なものには何にも依存していない。
「依存していない」というだけで良い理由としては充分だけど、具体的に良い理由を記述してみる。
※ほぼ上記の「悪い理由」の裏返し
開発でちょっとその部分だけ動かせる。
DIしていない場合、DBの設定が無いとHumanクラスをインスタンス化さえできなかったのに対し、DIしていれば以下のようにして実行できる。
# Brainインターフェースを実装したモッククラス。 class MockBrain implements Brain{ # なにも実装しない public void function __construct(){ } # なにも実装しない public void save(string keyword, string some_string){ } # 無条件に「おでん」と返す public string load(string keyword){ return "おでん" } } Brain mock_brain = new MockBrain() Human human = new Human("次郎", mock_brain) # ここがDI human.memolize("好きなおかず", "おでん") # なにも実行されない human.remember("好きなおかず") # "おでん" # 無条件に「おでん」と返ってくる。 human.getName() # 次郎
例えばHumanクラスのgetName()メソッドの挙動を見たい時、上記の様にMockBrainクラスを適当に作ってやれば動かせる。
開発でなんかデータが入っている状態を再現してコードを実行できる
上記のコードで「おでん」と保存されている状態を再現できている。
テストコード実行がDB無しで動く
上記のコードのように必要なテスト用Mockを作成すればテスト実行できる。
テストコード側もDBに依存しない
そもそもDBライブラリの機能はそれ単体でテストされているべきなので、Humanクラスのテストの範疇ではない。 Humanクラスとしては、DBライブラリへ渡る引数が意図通りであるかを担保できれば良い。
テスト用のMockライブラリを使えば「こういう値が渡ってくる」ことをテストできる。
テストコード側もDBに依存しなくなる。
DBライブラリを比較的簡単に入れ替えられる
Brainインターフェースで公開されている機能が変わらないことだけ担保できればいいので比較的簡単。
Humanクラス側には一切変更が入らない。
DbBrainクラスのテストがちゃんと書かれていればすぐに入れ替えられる。
場合によっては保存先を変えたりできる
保存先をDBだけではなくて、textにしたかったり、どこかのhostにapi送信にしたかったりした場合に以下のように簡単にできる。
# Brainインターフェースを実装したAPI送信クラス。 class ApiBrain implements Brain{ public void function __construct(){ this->api = new SomeApi() # なんらかのAPIライブラリ } public void save(string keyword, string some_string){ this->api.push(keyword, some_string) } public string load(string keyword){ return this->api.pull(keyword) } } Brain api_brain = new ApiBrain() Human human = new Human("次郎", api_brain) # ここがDI human.memolize("好きなおかず", "おでん") # api送信される human.remember("好きなおかず") # apiから「おでん」と返ってくる。 human.getName() # 次郎
新しくApiBrainクラスを用意しただけで、Humanクラスには一切変更はない。
DIコンテナとは?
今回のサンプルケースではDIする要素は1つだけだったが、これが増えてくるとHumanクラスをインスタンス化させることが大変になってくる。
この辺りの処理をうまいことまとめる方法としてDIコンテナがある。