はじめに
この記事は Java Advent Calendar 2018 の12日目の記事です。
JavaのDIに触れて半年程経ちます。急遽、チームの新メンバーへ教えることになったので、私の復習もかねて、記事にまとめます。
サンプルプロジェクトはこちらです。
https://github.com/segurvita/FatnessChecker
この記事のターゲット
クラスとかコンストラクターは理解できるけど、DIはよくわかっていない人を対象とします。
Bean
とか Interface
とかを知らなくても理解できるように、なるべくクラスとコンストラクターのみで解説しようと思います。
「DIとは依存性の注入という意味である」の意味がわからない
- 「DIとはDependency Injectionの略である」←ふむふむ
- 「Dependency Injectionとは、依存性の注入という意味である」←は???
- 「より厳密には依存性オブジェクトの注入である」←??
この 依存性の注入 の意味を理解することが、本記事の目的です。
DIとは、クラスの中で new
を使わないことである!
誤解を恐れずに言うならば、 DIとは、クラスの中で new
を使わないこと です。(正確な表現でないのは重々承知しておりますが、本記事はわかりやすさ重視で解説します。)
DIじゃないパターン
public class Hoge{
// フィールド(メンバー変数)
Fuga piyo;
// コンストラクター
public Hoge() {
// クラスの中でnewを使ってる。
this.piyo = new Fuga();
}
}
DIなパターン
public class Hoge{
// フィールド(メンバー変数)
Fuga piyo;
// コンストラクター
public Hoge(Fuga mogera) {
// newは使わない。外部から注入してる。
this.piyo = mogera;
}
}
※コンストラクターで注入してるのはあくまで一例です。外部から注入するならセッターでもいいです。
ただ、これだけだとメリットがいまいちわかりません。ここからはストーリー形式で解説します。
第1話:肥満度判定会社に行ってみた
突然はじまるストーリー
俺って太ってるのかな?そうだ!肥満度判定会社に検査を依頼してみよう!
数時間後
ようこそ、肥満度判定会社へ。本日はどのようなご用件でしょうか?
肥満度判定がしたいんです。
肥満度判定のお客様ですね。それでは、身長と体重を伺ってもよろしいでしょうか?
えっと、身長は170cm、体重は70kgです。
かしこまりました。少々お待ちくさい。弊社の最新型のロボットでBMIを計算いたします。
計算シマス・・・70kg÷1.70mデ、BMIハ 41.18 デス!
BMIが41.18ですと・・・40以上なので4度肥満ですね。
ええ!そんな!
数時間後
俺が肥満なんて絶対におかしい。あのロボット、バグってない?
BMIとは
BMI(体格指数)は、体重(kg)÷身長(m)÷身長(m)で求められます。
の計算は間違ってるかも?
肥満度判定とは
日本ではBMIが25以上の人は 肥満 と判定されます。肥満には1度~4度までの4段階あります。
画像引用:OMRON / vol.103 11年ぶりに肥満症の診断基準が改訂
各自の役割
第1話に登場した人物は以下の通りです。
-
は自分の肥満度が知りたい人です。利用者ということで、
User
と呼ぶことにします。 -
は肥満度判定会社の受付です。自分ではBMIを計算せず、 に計算を依頼します。BMIの数値をもとに、肥満度を判定することはできます。
FatnessChecker
と呼ぶことにします。 -
はBMIを計算するロボットです。まだ試作品でバグがあります。
BmiRobot
と呼ぶことにします。
第1話の事例の問題
BmiRobot
はBMIの計算を間違えてしまいました。どうやらバグがあるようです。
この間違った計算結果をもとに、 FatnessChecker
が肥満度判定を行ったため、判定結果も間違ったものになってしまいました。
つまり、 FatnessChecker
が BmiRobot
という信頼性の低いロボットに依存していたことが問題と言えます。
第2話:BMI仙人の登場
俺が肥満なんて絶対におかしい。あのロボット、バグってない?
そこの人、BMI計算でお困りですかな?
誰ですかあなたは!
私は、BMI仙人です。良ければ話を聞かせてくだされ。
ええ、実はかくかくしかじかで・・・
数日後
ようこそ、肥満度判定会社へ。(中略)身長と体重を伺ってもよろしいでしょうか?
身長は170cm、体重は70kgです。ただ、BMIの計算は、先日のロボットではなく、この人にやってもらいます。
ほっほっほ、この場合、70kg÷1.70m÷1.70mで、BMIは24.22ですな。
かしこまりました。BMIが24.22ですと・・・18.5以上25未満なので普通体重ですね。
やった!ギリギリ肥満じゃないや!
各自の役割
第2話に登場した人物は以下の通りです。
-
は自分の肥満度が知りたい人です。利用者ということで、
User
と呼ぶことにします。 -
はBMIを計算する達人です。たくさん訓練してきたので、絶対に計算を間違えません。
BmiMaster
と呼ぶことにします。 -
は を使って肥満度を判定する人です。 と同一人物ですが、説明のため区別し、
FatnessCheckerDi
と呼ぶことにします。
DIという解決策
User
は、自分が信頼できる人物 BmiMaster
にBMIの計算をやってもらう条件で、 FatnessCheckerDi
に肥満度判定を依頼しました。
これによって、 FatnessCheckerDi
の判定結果は、 BmiMaster
という信頼性の高い人物に依存することとなります。
これがDI(Dependency Injection、依存性の注入)です!つまり、
User
は、 FatnessCheckerDi
に対して、 BmiMaster
を注入したのです!
DIのメリット
DIのメリットは、 依存先の部品が完成していなくても、開発中の部品の単体テストができる ことです。
今回の事例ですと、 BmiRobot
にバグがあったから上手くいかなかっただけで、テストを十分に行い、 BmiRobot
のバグを減らしておけば、問題ないのではないか?という意見もあると思います。(私も当初そう思いました。)
しかし、そのテストを十分にするには、それなりの時間がかかります。
FatnessChecker
は BmiRobot
に依存しているので、 BmiRobot
が完成しない限り、 FatnessChecker
のテストをすることができません。これでは、開発時間が伸びてしまいます。
これに対し、 FatnessCheckerDi
は、依存する部品( BmiMaster
)を外部(User
)から注入できるようにしてあります。これによって、依存する部品( BmiMaster
)が完成していなくても、依存する部品( BmiMaster
)のモック(テスト専用のハリボテ)を注入すれば、 FatnessCheckerDi
の単体テストは可能になります。
DIのデメリット
デメリットは 学習コストが高い ことです。
チーム開発の場合、開発中のプロジェクトでDIを採用すると、メンバー全員がDIを学習する必要が出てきます。メンバーの入れ替わりが激しいチームだと、教育コストが無視できないレベルで高くなります。
サンプルコード
文章だけでは伝わりにくいと思いますので、Javaコードで説明します。
サンプルプロジェクトはこちらです。
https://github.com/segurvita/FatnessChecker
BmiRobot
で計算した時(DI導入前)
まず、 User
はこんな感じです。
/**
* 肥満度を知りたい人
*/
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
で、作業を依頼していますね。
次に、 FatnessChecker
は以下のようになります。
/**
* 肥満度判定会社(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()
を使い、 BmiRobot
を1台確保しています。
その後、 BmiRobot.calc(height, weight)
でBMI計算を依頼し、その計算結果をもとに肥満度を判定しています。
もし、 BmiRobot
にバグがあったら、判定結果もおかしくなる状況ですね。 BmiRobot
に依存しています。
その BmiRobot
は、たとえば以下のように実装します。
/**
* 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度肥満
となります。
BmiMaster
で計算した時(DI導入後)
まず、 User
はこんな感じです。
/**
* 肥満度を知りたい人
*/
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);
}
}
先ほどと大きく違うのは、 User
が new BmiMaster()
を使い、 BmiMaster
を1人確保していることです。これを、new FatnessCheckerDi(bmiCalculator)
で FatnessCheckerDi
に渡しています。
次に、 FatnessCheckerDi
は以下のようになります。
/**
* 肥満度判定会社(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
メソッドの中で、 BmiRobot
を1台確保していましたが、今回は、コンストラクターの引数で BmiMaster
を受け取り、それをフィールド(メンバー変数)に代入しています。
その後、this.bmiMaster.calc(height, weight)
でBMI計算を依頼し、その計算結果をもとに肥満度を判定しています。
今度は BmiMaster
に依存するようになりました。
その BmiMaster
は、たとえば以下のように実装します。
/**
* 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マスターあり):普通体重
となります。
おまけ:一体どこで new
すればいいの?
今回の話では、 User
が BmiMaster
を用意(new
)して FatnessCheckerDi
に注入しました。
それでは、 User
自体もDIパターンで設計されていた場合、BmiMaster
は誰が用意するのでしょうか?
User
の外にある別のクラスが用意するのでしょうか?そのクラスすらもDIパターンで設計されていたら?
そこで登場するのが DIコンテナー です。 new
の処理を一括して引き受けます。
Java Springの場合は、 BeanFactory
がこれに該当します。
さいごに
いかがでしたでしょうか。ストーリー仕立てでDIを説明してみました。
半年前に私がはじめてDIに触れたときは、 Autowired
や BeanFactory
といった、知らない用語に埋もれてしまい、「DI難しい・・・」となってしまいました。(当時、DIとDIコンテナーの区別がついていませんでした。)
しかし、半年コーディングしてみて、振り返ってみますと、「実は Bean
だとかを知らなくても、DIって説明できるのでは?」と思い、半年前の自分に向けてこの記事を書いてみました。
皆さまの参考になれば幸いです。
参考サイト
以下のサイトを参考にさせていただきました。