Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

はじめに

この記事は 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話:肥満度判定会社に行ってみた

突然はじまるストーリー

: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)÷身長(m)で求められます。

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

肥満度判定とは

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

画像引用:OMRON / vol.103 11年ぶりに肥満症の診断基準が改訂

各自の役割

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

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

第1話の事例の問題

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

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

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

第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 という信頼性の高い人物に依存することとなります。

これが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 はこんな感じです。

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マスターあり):普通体重

となります。

おまけ:一体どこで 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に触れたときは、 AutowiredBeanFactory といった、知らない用語に埋もれてしまい、「DI難しい・・・」となってしまいました。(当時、DIとDIコンテナーの区別がついていませんでした。)

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

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

参考サイト

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

segur
Unity認定プログラマー。VR学会認定技術者。xRエンジニア。ガジェットが大好きなギーク。 色んなコンピュータ言語に触れるのが好き。
https://segurvita.github.io/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away