依存性の注入(Dependency Injection)入門


Introuction

お久しぶりです。とりかつです。
普段、私はAndroidアプリの開発の学習をしているのですが、あるとき"DI"や"Dagger2"というライブラリに出会いました。しかしこのDIやライブラリの理解につまづき、非常に苦い思いをしました。具体的には以下の3つです。

  • 依存性の注入とは
  • なぜDependency Injection(以下DI)が必要なのか
  • なぜDagger2が必要なのか

今回の記事では自分と同じようにDIに苦しんでいる人の手助けになればと思いDIをテーマとしました。
記事の中で、私がDIの苦しみを乗り越える上で得た知見を可能な限り共有したいと思います。
なお今回の記事ではDagger2の具体的な使い方については紹介をしませんのでご了承ください。

Topics

  • 依存性の注入とかいうワード
  • なぜDIが必要なのか
  • なぜDagger2が必要となったのか
  • Dagger2を学習するのに参考になる資料
  • おわりに

「依存性の注入」とかいうワード

この章では「依存性の注入」という言葉を正しく捉えることを目的とします。
DIと出会った人はGoogle翻訳で「Dependency Injection」を翻訳すると思います。私は真っ先に翻訳しました笑。
その結果がこちらです。

ここでまず私は

「ん...?依存性の...注入...?」

ってなりました。
私はこの「依存性の注入」という訳がDIへの理解を阻む大きな要因の一つだと考えているため、「依存性の注入」という言葉をしっかりと噛み砕いていきたいと思います。

この言葉は「依存性」と「注入」という二つの単語に分けることができます。
ここからはそれぞれの単語に分けて解説を進めたいと思います。

「依存性」

まずは依存性という言葉を簡単なコードを扱いながら解説したいと思います。

以下のコードは車を表現したCarクラスとエンジンを表現したEngineクラスです。

//車クラス
class Car {
 //発進メソッド
 public void start () {
  //発進するためにはエンジンに点火をする必要がある
  Engine engine = new Engine();
  engine.ignition();
 }
}
//エンジンクラス
class Engine {
 //エンジン点火メソッド
 public void ignition() {
  //点火処理
 }
}

車は発進用メソッドrun()をもっています。また、エンジンは点火メソッドignition()を持っています。
車は発進する際にエンジンに点火する必要があるのでstart()の中でエンジンに点火をするignition()を呼び出しています。
このとき、「車クラスはエンジンクラスに依存している」と言います。もしエンジンでトラブルが起きたら車は発進できませんよね。

このように「依存」とはオブジェクト内で他のオブジェクトを呼び出したりしている状態のことを言います。
いま「依存」という言葉の意味が明らかになったので「依存性」という言葉について考えてみたいと思います。依存性は「依存する性質(を持ったもの)」と言い換えられると私は考えています。プログラムにおける「依存する性質を持ったもの」とはなんでしょうか。先ほどの車とエンジンの例では車というオブジェクトがエンジンというオブジェクトに依存していました。このことから
依存性=依存する性質を持ったもの=オブジェクト
と考えることができます。

現時点で明らかになった依存性という言葉を言い換えると
「依存性の注入」=「オブジェクトの注入」
となります。

「注入」

次に「注入」という言葉を簡単なコードを扱いながら解説します。以下先ほどの車クラスとエンジンクラスを改変したものです。

Car.java
//車クラス
class Car {
 //発進メソッド
 //メソッドないでエンジンをnewせずに引数でエンジンを受け取るように変更
 public void start (Engine engine) {
  //発進するためにはエンジンに点火をする必要がある
  engine.ignition();
 }
}
Engine.java
//エンジンクラス
class Engine {
 //エンジン点火メソッド
 public void ignition() {
  //点火処理
 }
}

変更点はCarクラスのstart()メソッドの中でエンジンのインスタンスを生成していた部分を、メソッドの引数でエンジンのインスタンスを受け取るようにしたことです。このように引数でインスタンスを渡すことを「注入」と言います。

これまでのことを踏まえると「依存性の注入」とは「オブジェクトを引数で渡すこと」と言い換えることができます。

なぜDIが必要なのか

前の章では「依存性の注入」が「オブジェクトを引数で渡すこと」だと解説をしました。ではなぜこのようなテクニックが必要になるのでしょうか。ここでも簡単なコードを扱いながら解説を試みます。

以下は本記事で一番最初に紹介した車クラスとエンジンクラスを表現したコードです。ここで車クラスはエンジンクラスに依存しているということを覚えていただければ幸いです。

Car.java
//車クラス
class Car {
 //発進メソッド
 public void start () {
  //発進するためにはエンジンに点火をする必要がある
  Engine engine = new Engine();
  engine.ignition();
 }
}
Engine.java
//エンジンクラス
class Engine {
 //エンジン点火メソッド
 public void ignition() {
  //点火処理
 }
}

たとえばエンジンを継承した性能のいいエンジン(NewEngine)が開発され、車のエンジンを交換したい場合があるとします。

このとき以下のように実装すれば車のエンジンを交換することが可能です。

Car.java
//車クラス
class Car {
 //発進メソッド
 public void start () {
  //発進するためにはエンジンに点火をする必要がある
  NewEngine engine = new NewEngine();
  engine.ignition();
 }
}
NewEngine.java
//新しいエンジンクラス
class NewEngine extends Engine {
 //エンジン点火メソッド
 public void ignition() {
  //点火処理
 }
}
Engine.java
//エンジンクラス
class Engine {
 //エンジン点火メソッド
 public void ignition() {
  //点火処理
 }
}

ここでのコードの変更点は以下の2つです。

  • 車クラスでnew Engine => new NewEngine
  • NewEngineクラスの作成

このとき、新しいエンジンクラスを使うために車クラスのコードを変更しました。

このように新しいエンジンを作って使う際には毎回車クラスのコードを変更する必要があります。今回の関心ごとは新しいエンジンを作成して使うことなのに車クラスのことにまで注意を払わないといけません。

また別のケースで車クラスのテストをしたい場合を考えます。車クラスはエンジンに依存しているためエンジンクラスが完成するまで動作を確認することができません。もし車クラスとエンジンクラスの実装を別の人が行っていたらエンジンクラスが完成するまで車クラスの実装を担当する人の作業がストップしてしまいます。

では次のように車クラスのコードを変更した場合はどうでしょうか。

Car.java
//車クラス
class Car {
 Engine engine;

 //コンストラクタ
 Car(Engine engine) {
  this.engine = engine
 }

 //発進メソッド
 public void start () {
  //発進するためにはエンジンに点火をする必要がある
  engine.ignition();
 }
}

このコードではコンストラクタの引数でEngineのインスタンスを受け取るように変更をし、車クラスとエンジンクラスの依存を解消しました。
この場合、新しく作るエンジンクラスがEngineクラスを継承しており、ignition()メソッドを持っていれば車クラスのコードを変更する必要はなさそうです。

また車クラスをテストする際にも、テストようにEngineクラスを継承しており、ignition()メソッドを持っているEngineクラスのモックをテストの中で作成すれば車クラスのテストが可能になります。

ここで挙げたようなメリットを享受するためにDIが必要とされます。

なぜDagger2が必要となのか

前の章ではDIを行うことによってオブジェクト間の依存を減らすことができ、様々なメリットを教授することができました。私はこのDIのメリットに気づいた時に「DIすごっ」ってなりましたが、同時にある不安を覚えました。
ここで前の章の最後のコードスニペットを再掲します。

Car.java
//車クラス
class Car {
 Engine engine;

 //コンストラクタ
 Car(Engine engine) {
  this.engine = engine
 }

 //発進メソッド
 public void start () {
  //発進するためにはエンジンに点火をする必要がある
  engine.ignition();
 }
}

この車クラスにエンジンクラスを渡す時

Main.java
Engine engine = new Engine();
new Car(engine);

のようにしてエンジンを渡す必要があります。ではこのエンジンクラスのインスタンスはどこで生成したらいいでしょうか。

  • 車クラスを利用するクラスでもDIを行い、エンジンをコンストラクタで受け取るようにします。
  • さらに車クラスを利用するクラスを利用するクラスでもエンジンをコンストラクタで受け取るようにします。
  • さらに車クラスを利用するクラスを利用するクラスを利用するクラスでもエンジンをコンストラクタで受け取るよう...

このようにDI初心者だった私はどこでインスタンスを生成するのかとても不安でした。

この解決策としてインスタンスを生成する役割を一箇所にあつめたクラスをつくり、生成処理はそのクラスに任せてしまうことが考えられます。

しかしこの方法にも問題が存在します。生成するインスタンスの数がそこまで多くなければこの方法でいいのですが、生成するインスタンスの種類が200、300となってくると人間では管理をすることが難しくなってきます。

この問題を解決してくれるのがDagger2です。Dagger2はアノテーションをつかってどこにどのインスタンスを注入するかということを予め登録しておくことで、コンパイル時に自動でインスタンスを注入してくれる便利なライブラリです。

DI、Dagger2を学習するのに参考になる資料

Master of Dagger あんざいゆき新刊
この本はDagger2の仕組みや使い方をとても丁寧に解説してくれているのでとてもおすすめです。
またAndroidアプリのプロジェクトでのDaggerの使いかたについても具体例を上げながら解説されています。

とてもつもなくわかりやすいdagger2(2.11)入門
めちゃめちゃわかりやすいです。初めてDIとかを触る人の目線で解説が進んでいくのでDIについて知らなくても最後まで読むことができました。

おわりに

今回はDIとはなにかという部分からDagger2の必要性について解説をしました。
より正確な情報を多くの人に伝えたいため、もし記事内で間違っている部分がありましたらご指摘いただければ幸いです。