Java
DI
JavaDay 12

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


はじめに

この記事は Java Advent Calendar 2018 の12日目の記事です。

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

サンプルプロジェクトはこちらです。

https://github.com/segurvita/FatnessChecker


この記事のターゲット

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

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


肥満度判定物語

突然はじまるストーリー

: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: 俺が肥満なんて絶対におかしい。あのロボット、バグってない?

:older_man: そこの人、肥満度判定でお困りですかな?良ければ話を聞かせてくだされ。

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

数日後

:information_desk_person: ようこそ、肥満度判定会社へ。さっそくですが、身長と体重を伺ってもよろしいでしょうか?

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

:information_desk_person: 少々お待ちくさい・・・最新ロボットでBMIを計算いたします。

:man: ちょっと待って!BMIの計算はこの人にやってもらいたいんだ。

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

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

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


各自の役割

このストーリーに登場した人物は以下の通りです。



  • :man: は自分の肥満度が知りたい人です。利用者ということで、Userと呼ぶことにします。


  • :person_frowning::robot: を使って肥満度を判定する人です。 FatnessChecker と呼ぶことにします。


  • :robot: はBMIを計算するロボットです。まだ試作品でバグがあります。 BmiRobot と呼ぶことにします。


  • :older_man: はBMIを計算する達人です。絶対に計算を間違えません。 BmiMaster と呼ぶことにします。


  • :information_desk_person::older_man: を使って肥満度を判定する人です。:person_frowning: と同一人物ですが、説明のため区別し、 FatnessCheckerDi と呼ぶことにします。


今回の事例の問題

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

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

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


DIという解決策

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

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

これがDI(Dependency Injection、依存性の注入)です!つまり、


:man: User は、:information_desk_person: FatnessCheckerDi に対して、 :older_man: BmiMaster を注入したのです!(意味深)


サンプルコード

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

サンプルプロジェクトはこちらです。

https://github.com/segurvita/FatnessChecker


:robot: BmiRobot で計算した時(DI導入前)

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


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 は以下のようになります。


FatnessChecker.java

/**

* 肥満度判定会社(BMIロボット利用)
*/

public class FatnessChecker {
/**
* BMIを判定する
* @param height 身長 [cm]
* @param weight 体重 [kg]
* @return 肥満度
*/

public String check(double height, double weight) {
// 社内の最新型BMIロボットを1台確保する。
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 "1度肥満";
} else if (bmi < 35.0) {
return "2度肥満";
} else if (bmi < 40.0) {
return "3度肥満";
} else {
return "4度肥満";
}
}
}


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

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

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

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


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] となっていますが、これは間違っています。

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


実行結果

肥満度判定結果(BMIマスターなし):4度肥満


となります。


:older_man: BmiMaster で計算した時(DI導入後)

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


User.java

/**

* 肥満度を知りたい人
*/

public class User {
/**
* BMIマスターありで肥満度判定会社に判定を依頼する。
*/

public void runWithBmiMaster() {
// BMIマスターを1人確保する。
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: Usernew BmiMaster()を使い、:older_man: BmiMaster を1人確保していることです。これを、new FatnessCheckerDi(bmiCalculator):information_desk_person: FatnessCheckerDi に渡しています。

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


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 "1度肥満";
} else if (bmi < 35.0) {
return "2度肥満";
} else if (bmi < 40.0) {
return "3度肥満";
} else {
return "4度肥満";
}
}
}


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

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

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

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

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


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 となっています。これは正しい数式です。

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


実行結果

肥満度判定結果(BMIマスターあり):普通体重


となります。


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のメリットの1つです。


さいごに

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

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

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

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


参考サイト

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