肥満な物語でDIを解説する

# はじめに

この記事は [Java Advent Calendar 2018](https://qiita.com/advent-calendar/2018/java) の12日目の記事です。

JavaのDIに触れて半年程経ちます。急遽、チームの新メンバーへ教えることになったので、私の復習もかねて、記事にまとめます。

サンプルプロジェクトはこちらです。
https://github.com/segurvita/FatnessChecker



# この記事のターゲット

クラスとかコンストラクターは理解できるけど、DIはよくわかっていない人を対象とします。

`Bean` とか `Interface` とかを知らなくても理解できるように、なるべくクラスとコンストラクターのみで解説しようと思います。




# 「DIとは依存性の注入という意味である」の意味がわからない

- 「DIとはDependency Injectionの略である」←ふむふむ
- 「Dependency Injectionとは、依存性の注入という意味である」←は？？？
- 「より厳密には依存性オブジェクトの注入である」←？？

この **依存性の注入** の意味を理解することが、本記事の目的です。



# DIとは、クラスの中で `new` を使わないことである！

誤解を恐れずに言うならば、 **DIとは、クラスの中で `new` を使わないこと** です。（正確な表現でないのは重々承知しておりますが、本記事はわかりやすさ重視で解説します。）

### DIじゃないパターン

```java
public class Hoge{
    // フィールド（メンバー変数）
    Fuga piyo;
    
	// コンストラクター
	public Hoge() {
	    // クラスの中でnewを使ってる。
		this.piyo = new Fuga();
	}
}
```

### DIなパターン

```java
public class Hoge{
    // フィールド（メンバー変数）
    Fuga piyo;
    
	// コンストラクター
	public Hoge(Fuga mogera) {
	    // newは使わない。外部から注入してる。
		this.piyo = mogera;
	}
}
```

※コンストラクターで注入してるのはあくまで一例です。外部から注入するならセッターでもいいです。

ただ、これだけだとメリットがいまいちわかりません。ここからはストーリー形式で解説します。




# 第1話：肥満度判定会社に行ってみた

突然はじまるストーリー

:man: 俺って太ってるのかな？そうだ！肥満度判定会社に検査を依頼してみよう！

数時間後

:person_frowning: ようこそ、肥満度判定会社へ。本日はどのようなご用件でしょうか？

:man: 肥満度判定がしたいんです。

:person_frowning:  肥満度判定のお客様ですね。それでは、身長と体重を伺ってもよろしいでしょうか？

:man: えっと、身長は170cm、体重は70kgです。

:person_frowning: かしこまりました。少々お待ちくさい。弊社の最新型のロボットでBMIを計算いたします。

:robot: 計算シマス・・・70kg÷1.70mデ、BMIハ **41.18** デス！

:person_frowning: BMIが41.18ですと・・・40以上なので**4度肥満**ですね。

:man: ええ！そんな！

数時間後

:man: 俺が肥満なんて絶対におかしい。あのロボット、バグってない？



## BMIとは

BMI（体格指数）は、体重（kg）÷身長（m）÷身長（ｍ）で求められます。

:robot: の計算は間違ってるかも？



## 肥満度判定とは

日本ではBMIが25以上の人は **肥満** と判定されます。肥満には1度～4度までの4段階あります。

![](https://www.healthcare.omron.co.jp/src/column/tpx/103_13_4.gif)

画像引用：[OMRON / vol.103　11年ぶりに肥満症の診断基準が改訂](https://www.healthcare.omron.co.jp/resource/column/topics/103.html)



## 各自の役割

第1話に登場した人物は以下の通りです。

- :man: は自分の肥満度が知りたい人です。利用者ということで、`User`と呼ぶことにします。
- :person_frowning: は肥満度判定会社の受付です。自分ではBMIを計算せず、 :robot: に計算を依頼します。BMIの数値をもとに、肥満度を判定することはできます。  `FatnessChecker` と呼ぶことにします。
- :robot: はBMIを計算するロボットです。まだ試作品でバグがあります。 `BmiRobot` と呼ぶことにします。



## 第1話の事例の問題

:robot:` BmiRobot` はBMIの計算を間違えてしまいました。どうやらバグがあるようです。

この間違った計算結果をもとに、:person_frowning: `FatnessChecker` が肥満度判定を行ったため、判定結果も間違ったものになってしまいました。

つまり、:person_frowning: `FatnessChecker` が :robot: `BmiRobot` という信頼性の低いロボットに依存していたことが問題と言えます。

<img width=400 src="https://qiita-image-store.s3.amazonaws.com/0/221845/b90406dc-c101-decb-09a5-283fde0a4898.png" />





# 第2話：BMI仙人の登場

:man: 俺が肥満なんて絶対におかしい。あのロボット、バグってない？

:older_man: そこの人、BMI計算でお困りですかな？

:man: 誰ですかあなたは！

:older_man: 私は、BMI仙人です。良ければ話を聞かせてくだされ。

:man: ええ、実はかくかくしかじかで・・・

数日後

:information_desk_person: ようこそ、肥満度判定会社へ。（中略）身長と体重を伺ってもよろしいでしょうか？

:man: 身長は170cm、体重は70kgです。ただ、BMIの計算は、先日のロボットではなく、この人にやってもらいます。

:older_man: ほっほっほ、この場合、70kg÷1.70m÷1.70mで、BMIは**24.22**ですな。

:information_desk_person: かしこまりました。BMIが24.22ですと・・・18.5以上25未満なので**普通体重**ですね。

:man: やった！ギリギリ肥満じゃないや！



## 各自の役割

第2話に登場した人物は以下の通りです。

- :man: は自分の肥満度が知りたい人です。利用者ということで、`User`と呼ぶことにします。
- :older_man: はBMIを計算する達人です。たくさん訓練してきたので、絶対に計算を間違えません。 `BmiMaster` と呼ぶことにします。
- :information_desk_person: は :older_man: を使って肥満度を判定する人です。:person_frowning: と同一人物ですが、説明のため区別し、 `FatnessCheckerDi` と呼ぶことにします。



## DIという解決策

:man: `User`は、自分が信頼できる人物 :older_man:`BmiMaster` にBMIの計算をやってもらう条件で、:information_desk_person: `FatnessCheckerDi` に肥満度判定を依頼しました。

これによって、:information_desk_person: `FatnessCheckerDi` の判定結果は、:older_man: `BmiMaster` という信頼性の高い人物に依存することとなります。

<img width=400 src="https://qiita-image-store.s3.amazonaws.com/0/221845/1c07cecb-45b3-2dfa-0743-8a5552a7a41d.png" />

これが**DI**（Dependency Injection、依存性の注入）です！つまり、

### :man: `User` は、:information_desk_person: `FatnessCheckerDi` に対して、 :older_man: `BmiMaster`  を注入したのです！



# DIのメリット

DIのメリットは、 **依存先の部品が完成していなくても、開発中の部品の単体テストができる** ことです。

今回の事例ですと、:robot: `BmiRobot` にバグがあったから上手くいかなかっただけで、テストを十分に行い、:robot: `BmiRobot` のバグを減らしておけば、問題ないのではないか？という意見もあると思います。（私も当初そう思いました。）

しかし、その**テストを十分にする**には、それなりの時間がかかります。

:person_frowning: `FatnessChecker` は :robot:  `BmiRobot` に依存しているので、:robot: `BmiRobot` が完成しない限り、 :person_frowning: `FatnessChecker` のテストをすることができません。これでは、開発時間が伸びてしまいます。

これに対し、:information_desk_person: `FatnessCheckerDi` は、依存する部品（:older_man: `BmiMaster`）を外部（:man:`User`）から注入できるようにしてあります。これによって、依存する部品（:older_man: `BmiMaster`）が完成していなくても、依存する部品（:older_man: `BmiMaster`）のモック（テスト専用のハリボテ）を注入すれば、:information_desk_person: `FatnessCheckerDi` の単体テストは可能になります。



# DIのデメリット

デメリットは **学習コストが高い** ことです。

チーム開発の場合、開発中のプロジェクトでDIを採用すると、メンバー全員がDIを学習する必要が出てきます。メンバーの入れ替わりが激しいチームだと、教育コストが無視できないレベルで高くなります。



# サンプルコード

文章だけでは伝わりにくいと思いますので、Javaコードで説明します。

サンプルプロジェクトはこちらです。
https://github.com/segurvita/FatnessChecker



## :robot: `BmiRobot` で計算した時（DI導入前）

まず、 :man: `User` はこんな感じです。

```java:User.java
/**
 * 肥満度を知りたい人
 */
public class User {
	/**
	 * BMIマスターなしで肥満度判定会社に判定を依頼する。
	 */
	public void runWithoutBmiMaster() {
		// 肥満度判定会社の人と会話を始める。
		FatnessChecker fatnessChecker = new FatnessChecker();

		// 身長と体重を伝え、肥満度の判定を依頼する。
		String result = fatnessChecker.check(170.0, 70.0);

		// 肥満度の判定結果を表示する。
		System.out.println("肥満度判定結果（BMIマスターなし）：" + result);
	}
}
```

`fatnessChecker.check` で、作業を依頼していますね。

次に、 :person_frowning: `FatnessChecker` は以下のようになります。

```java:FatnessChecker.java
/**
 * 肥満度判定会社（BMIロボット利用）
 */
public class FatnessChecker {
	/**
	 * BMIを判定する
	 * @param height 身長 [cm]
	 * @param weight 体重 [kg]
	 * @return 肥満度
	 */
	public String check(double height, double weight) {
		// 社内の最新型BMIロボットを１台確保する。
		BmiRobot bmiRobot = new BmiRobot();

		// BMIロボットにBMI計算を依頼する。
		double bmi = bmiRobot.calc(height, weight);

		// BMI計算結果から肥満度を判定する。
		if (bmi < 18.5) {
			return "低体重";
		} else if (bmi < 25.0) {
			return "普通体重";
		} else if (bmi < 30.0) {
			return "１度肥満";
		} else if (bmi < 35.0) {
			return "２度肥満";
		} else if (bmi < 40.0) {
			return "３度肥満";
		} else {
			return "４度肥満";
		}
	}
}
```

`check` メソッドの中で、`new BmiRobot()` を使い、:robot: `BmiRobot` を1台確保しています。

その後、 `BmiRobot.calc(height, weight)` でBMI計算を依頼し、その計算結果をもとに肥満度を判定しています。

もし、:robot: `BmiRobot` にバグがあったら、判定結果もおかしくなる状況ですね。:robot: `BmiRobot` に依存しています。

その :robot: `BmiRobot` は、たとえば以下のように実装します。

```java:BmiRobot.java
/**
 * BMIロボット（バグあり）
 */
public class BmiRobot {
	/**
	 * BMIを計算する 
	 * @param height 身長 [cm]
	 * @param weight 体重 [kg]
	 * @return BMI
	 */
	public double calc(double height, double weight) {
		// 体重[kg] ÷ 身長[m]
		return weight * 100 / height;
	}
}
```

数式が `体重[kg] ÷ 身長[m]` となっていますが、これは間違っています。

この状態でプログラムを実行すると、結果は

```bash:実行結果
肥満度判定結果（BMIマスターなし）：４度肥満
```

となります。



##  :older_man: `BmiMaster` で計算した時（DI導入後）

まず、 :man: `User` はこんな感じです。

```java:User.java
/**
 * 肥満度を知りたい人
 */
public class User {
	/**
	 * BMIマスターありで肥満度判定会社に判定を依頼する。
	 */
	public void runWithBmiMaster() {
		// BMIマスターを１人確保する。
		BmiMaster bmiCalculator = new BmiMaster();

		// 肥満度判定会社の人に、BMI計算をBMIマスターにしてもらうよう依頼する。
		FatnessCheckerDi fatnessCheckerDi = new FatnessCheckerDi(bmiCalculator);

		// 身長と体重を伝え、肥満度の判定を依頼する。
		String result = fatnessCheckerDi.check(170.0, 70.0);

		// 肥満度の判定結果を表示する。
		System.out.println("肥満度判定結果（BMIマスターあり）：" + result);
	}
}
```

先ほどと大きく違うのは、:man: `User` が `new BmiMaster()`を使い、:older_man: `BmiMaster` を1人確保していることです。これを、`new FatnessCheckerDi(bmiCalculator)`で :information_desk_person: `FatnessCheckerDi` に渡しています。

次に、:information_desk_person: `FatnessCheckerDi` は以下のようになります。

```java:FatnessCheckerDi.java
/**
 * 肥満度判定会社（BMIマスター利用）
 */
public class FatnessCheckerDi {
	/**
	 * BMIマスター
	 */
	final private BmiMaster bmiMaster;

	/**
	 * コンストラクタ
	 * @param bmiCalculator ユーザーから指名されたBMIマスター
	 */
	public FatnessCheckerDi(BmiMaster bmiMaster) {
		// ユーザーから指名されたBMIマスターを迎える。
		this.bmiMaster = bmiMaster;
	}

	/**
	 * BMIを判定する
	 * @param height 身長 [cm]
	 * @param weight 体重 [kg]
	 * @return 肥満度
	 */
	public String check(double height, double weight) {
		// ユーザーから指名されたBMIマスターにBMI計算を依頼する。
		double bmi = this.bmiMaster.calc(height, weight);

		// BMI計算結果から肥満度を判定する。
		if (bmi < 18.5) {
			return "低体重";
		} else if (bmi < 25.0) {
			return "普通体重";
		} else if (bmi < 30.0) {
			return "１度肥満";
		} else if (bmi < 35.0) {
			return "２度肥満";
		} else if (bmi < 40.0) {
			return "３度肥満";
		} else {
			return "４度肥満";
		}
	}
}
```

先ほどと大きく違うのはコンストラクターが登場したことです。

先ほどは、`check`メソッドの中で、:robot: ` BmiRobot` を1台確保していましたが、今回は、コンストラクターの引数で :older_man: `BmiMaster` を受け取り、それをフィールド（メンバー変数）に代入しています。

その後、`this.bmiMaster.calc(height, weight)` でBMI計算を依頼し、その計算結果をもとに肥満度を判定しています。

今度は :older_man: `BmiMaster` に依存するようになりました。

その :older_man: `BmiMaster` は、たとえば以下のように実装します。

```java:BmiMaster.java
/**
 * BMIマスター（達人）
 */
public class BmiMaster {
	/**
	 * BMIを計算する
	 * @param height 身長 [cm]
	 * @param weight 体重 [kg]
	 * @return BMI
	 */
	public double calc(double height, double weight) {
		// 体重[kg] ÷ (身長[m])^2
		return weight * 10000 / (height * height);
	}
}
```

数式が `体重[kg] ÷ (身長[m])^2` となっています。これは正しい数式です。

この状態でプログラムを実行すると、結果は

```bash:実行結果
肥満度判定結果（BMIマスターあり）：普通体重
```

となります。



# おまけ：一体どこで `new` すればいいの？

今回の話では、:man: `User`が :older_man:`BmiMaster` を用意（`new`）して:information_desk_person: `FatnessCheckerDi`に注入しました。

それでは、:man: `User`自体もDIパターンで設計されていた場合、:older_man:`BmiMaster`は誰が用意するのでしょうか？

:man: `User` の外にある別のクラスが用意するのでしょうか？そのクラスすらもDIパターンで設計されていたら？

そこで登場するのが **DIコンテナー** です。 `new` の処理を一括して引き受けます。

Java Springの場合は、 `BeanFactory`　がこれに該当します。



# さいごに

いかがでしたでしょうか。ストーリー仕立てでDIを説明してみました。

半年前に私がはじめてDIに触れたときは、 `Autowired` や `BeanFactory` といった、知らない用語に埋もれてしまい、「DI難しい・・・」となってしまいました。（当時、DIとDIコンテナーの区別がついていませんでした。）

しかし、半年コーディングしてみて、振り返ってみますと、「実は `Bean` だとかを知らなくても、DIって説明できるのでは？」と思い、半年前の自分に向けてこの記事を書いてみました。

皆さまの参考になれば幸いです。



# 参考サイト

以下のサイトを参考にさせていただきました。

- [DI・DIコンテナ、ちゃんと理解出来てる・・？](https://qiita.com/ritukiii/items/de30b2d944109521298f)
- [TutorialsTeacher Dependency Injection](http://www.tutorialsteacher.com/ioc/dependency-injection)
- [SpringのDIコンテナの動作イメージ(雰囲気)を掴もう](https://qiita.com/kazuki43zoo/items/7a0e96573e930ac934ed)
- [何故DIコンテナが必要なのか](https://qiita.com/kabosusoba/items/200d14c68b8c0e5b2092)