👌

DI(依存性の注入)とは依存性を注入するということである、、?

2021/03/28に公開

2020/4/6更新:DIの説明に関して誤解を招く表現があったため内容を更新しました

はじめに

あなたはDI(依存性の注入)という言葉を初めて聞いたときに、その意味をイメージできましたか?
私はできませんでした。
何に対する依存性を何に注入する、、?
そもそも「依存性」って何?(実装したことがない!)
と、次から次へと疑問が溢れ出てきました。

私が初めて DI という言葉を聞いたのは、新卒で入社して初めて案件(Androidアプリ)にアサインされたときです。
その案件ではDagger2を用いた DI が実装されており
私自身も見様見真似で「DIなるもの」の実装を行いました。

しかし、そもそも DI とは何かをきちんと理解できていなかったため、ただただ記述するコードが増えて複雑だな、面倒だなと感じていました。

それから月日が経ち Dagger2 の復習をしようとしましたが、まず DI とは何かを理解しないと Dagger2 も理解できないだろうと考え、今回はこの記事を書くことを通して DI を理解しようと考えました。

この記事での進め方

この記事では、私が DI について調べた経緯をそのまま書いていこうと思います。
このような構成にした理由は、自分が何に疑問を持っていたのかを把握することで、初心者にとって理解しやすい文章を書けるのではないか、後輩に教える際に役立つのではないかと考えたからです。

DIについての説明だけを読みたい方は、こちら:Wikipediaを読んでをご覧ください。

対象者

この記事の読者として想定しているのは、以下のような人たちです。

  • DI という言葉を最近初めて聞いた人
  • DI に関して調べてはみたもののイマイチ理解できていない人
  • Dagger2 とか使ったことあるけど、結局あれは何をやってたんやろ?と疑問を持っている人

DI(依存性の注入)とは依存性を注入するということである。

なんやねん、結局意味分からんやんと思った方、
安心してください、この記事を読み終える頃には「なるほどー」と思ってもらえるよう頑張ります。

では、私が学習した経緯を記載していきます。

Daggerを理解しようとした

  • 調査した資料:Dagger ー公式リファレンス
  • この資料を調査したきっかけ:前述の通り、Dagger2の学習をしようとしていたから
  • 結果:そもそも DI とは何かが分からなくなった

DI をきちんと理解できていないことに気づき、DI について調べることにしました。

Google公式リファレンスを見に行く

  • 調査した資料:アプリのアーキテクチャガイド ーGoogle公式ガイド
  • この資料を調査したきっかけ:
    Dagger を調査していた流れで Google のリファレンスに説明されていないか見てみた
    (また、概念的なものは公式のリファレンスをきちんと読むのが一番正確で、一番早いと個人的に思っているため、Google公式リファレンスを見てみた)
  • 結果:説明があった!さらに、Wikipedia を参照していることが分かった!

そこで、Wikipedia の説明を読むことにしました

Wikipediaの説明を見に行く

  • 調査した資料:Dependency Injection, 依存性の注入ーWikipedia
  • この資料を調査したきっかけ:あの Googleさんが参照していたから(参照されていたのは英語版)
  • 結果:DI ができた経緯などが簡潔にまとめられていて理解することができた!

    Dagger などの DIコンテナ? を使用しなくとも DI を実装できることが分かった

日本語版 Wikipedia を何度か読んだ結果、DI についておおよそ理解することができました!
なので、まずは Wikipedia をきちんと読んでみることをオススメします。

Wikipediaを読んで

DI誕生の経緯

DI誕生の経緯を簡単に話します。
なお、ここでは Kotlin で記述します。

まずは、DI せずに、あるオブジェクト内で外部のオブジェクトをインスタンス化してしまっている場合を見てみます。

DI していない場合:


class Apple(val hasPoison: Boolean = false)

// 食品クラス. 抽象的な概念であるべきですが、リンゴ(Apple)に依存してしまっています.
class Food {
    fun eat() {
        val apple = Apple()
        if (apple.hasPoison) {
            println("ゲームオーバー")
        } else {
            println("リンゴ美味しかったー!")
        }
    }
}

class Human {
    // このメソッドからeat()メソッドを呼び出す. テストコードに相当する部分でもあります.
    fun doSomething() {
        val food = Food()
        food.eat()
    }
}

doSomething() 実行結果は常に

ゲームオーバー

問題点:

  • Foodクラスのeat()メソッドにて、外部のAppleクラスをインスタンス化している

    つまり、Foodクラスのeat()メソッドはAppleクラスに依存している

なぜ問題か:

  • eat()メソッドの処理内容を直接編集するか、Appleクラスを編集しない限り、eat()メソッドの処理結果は変化しないから。

    食品(Food)はリンゴ以外にバナナやチーズもあるはずで、さらにリンゴの中にも毒入りと毒なし2種類あるはずなのに、食品(Food)を食べる(eat)ときの結果は常に同じ挙動(常に「リンゴ美味しかったー」)になってしまうから。
  • 単体テストを行いづらいから。
    例えば、eat()メソッド内でif文に問題がないかなどを確認するためには、Appleクラスを書き換えるしかありません。これではテストコード(doSomething()メソッド)だけでテストすることは不可能になってしまうのです。

そこで思います。

こんなメソッドどこで使うねん!
どうやってテストするねん!
柔軟性がなさすぎる。。と
-> よし、eat()メソッドのAppleクラスに対する依存性をひとまず弱くして、あとで DI(依存性の注入)するようにしよう!

これが、DI誕生の経緯です。

以下に DI誕生の経緯を抽象的に説明し直します。

あるオブジェクトAがオブジェクトBに依存してしまう、つまりオブジェクトAが別のオブジェクトBを持ってしまうのは、使用する際の柔軟性のなさやテストのしにくさの観点から見て、良くない。
そこで、まずオブジェクトAがオブジェクトBを持たないようにし、
外部からオブジェクトAにオブジェクトBを注入してあげようというのが DI の考え方です。

まだ、あまりピンとこないと思うので
DI の考え方を取り入れた簡単な例を以下に示します。

DIした場合:


// Foodクラス・eat()メソッドの Apple に対する依存性をなくし、Foodクラス・eat()メソッドを抽象化します
interface Food {
    fun eat()
}

// Food インターフェースを批准させることで、eat()メソッドを Appleクラス用に実装します
class Apple(val hasPoison: Boolean) : Food {
    override fun eat() {
        if (hasPoison) {
            println("ゲームオーバー")
        } else {
            println("リンゴ美味しかったー!")
        }
    }
}

class Human {
    // このメソッドからeat()メソッドを呼び出す. テストコードに相当する部分でもあります.
    fun doSomething() {
        val apple = Apple(hasPoison = false)
        val poisonApple = Apple(hasPoison = true)
        apple.eat()
        poisonApple.eat()
    }
}

doSomething() 実行結果:

リンゴ美味しかったー! // apple.eat() の実行結果
ゲームオーバー        // poisonApple.eat() の実行結果

ちなみに、食品(Food)インターフェースはリンゴ(Apple)に依存しない抽象的な概念であるため、以下のようにバナナ(Banana)も作れます


class Banana : Food {
    override fun eat() {
        println("バナナを食べると元気が出る")
    }
}

Banana().eat() の実行結果:


バナナを食べると元気が出る

DIには種類がある

Wikipediaでは1行ずつで簡潔にまとめられていますが、自分なりに補足してみました。

  • Interface Injection:前項の例がこれに当たります

    この方法では、インターフェース、つまり土台となるメソッド群を定義し、作成するクラスにそのインターフェースを批准させることで DIを実装します。

    Java/ Kotlinでいうと interface、Objective-C/ Swiftでいうと protocol がインターフェースに該当すると思います。
  • Constructor Injection

    あるクラスAのコンストラクタとしてその変数 hoge をインスタンス化することで、クラスAにHogeクラスの「依存性」を「注入」する
  • Setter Injection

    あるクラス内のプロパティに他クラスのインスタンスとなるものを宣言だけしておき(var hoge: Hoge?)

    外部からアクセス可能なsetterメソッド経由で hoge をインスタンス化することで、Hogeクラスの「依存性」を「注入」する

番外編: 今までやっていたあれも実はDIのひとつだった?

今回の例では、Interface Injection という方法で DIを実装しました。
しかし、似たようなことは誰もが当たり前のように実装しているのです。

前項で使用したeat()メソッドを用いて説明します。

DIしていない場合:


fun eat() {
    val apple = Apple()
    if (apple.hasPoison) {
        println("ゲームオーバー")
    } else {
        println("リンゴ美味しかったー!")
    }
}

DIの考え方を少し意識した場合:


fun eat(apple: Apple) {
    if (apple.hasPoison) {
        println("ゲームオーバー")
    } else {
        println("リンゴ美味しかったー!")
    }
}

何が変わったのでしょうか?

Appleクラスを引数で渡すよう修正しただけです。

「どんな」リンゴ(Apple)を食べるかを代入できるようにした、つまり外部から注入できるようにした、という意味では、
今まで普通にやっていたこんなことも実は・・・
DI のひとつ? だったのです。

「依存性」の言い換え

「依存性」という言葉が理解の妨げとなっているのではないかと思い、以下のような言い換えを考えてみました。

依存性の注入における「依存性」=

  • オブジェクト

    実際に英語版 Wikipediaでは、以下のように「依存性」は「オブジェクト」であると記載されています。

A "dependency" is an object that can be used

  • インスタンス
  • Java/ Kotlinなどで言うところのinterfaceで定義されたメソッドの処理内容を実際に記述しているメソッド
  • Objective-C/ Swiftで言うところのprotocolを批准しているクラス内で処理内容を実際に記述しているデリゲートメソッド

次に「注入」を付け足して「依存性の注入」という言葉を言い換えてみます。

「依存性の注入」とは

(オブジェクトAの外部から)「オブジェクトAで必要な、オブジェクトBの実態(インスタンスや具体的なメソッド)」を「注入/代入/設定/提供」することである

と言葉を付け足して説明してみました。
なお、ここでオブジェクトはクラスやメソッドに該当します。

スッキリしましたでしょうか?
これだけだと、まだモヤッとするかもしれません。

ここで、前述した3つの DI手法に応じて言い換えてみます。

  • Interface Injection

    メソッドAが定義されたインターフェースを批准したクラスBで、メソッドAの具体的な処理を実装することである
  • Constructor Injection

    クラスAのコンストラクタとしてクラスBを設定し、クラスA生成時にクラスBを設定することである
  • Setter Injection

    まず、クラスAのプロパティとしてクラスBを設定し、そのクラスBのセッターメソッドも定義する

    そして、クラスA生成後にクラスBのセッターメソッドを呼び出すことでクラスBを設定することである

少し、スッキリできたでしょうか?
念のためこちらにも Interface Injection の手法を用いた DIの例を添付しておきます。

DIした場合:


interface Food {
    fun eat()  // <- メソッドA
}

// Apple <- クラスB
class Apple(val hasPoison: Boolean) : Food {
    // 以下、「メソッドAの具体的な処理」
    override fun eat() {
        if (hasPoison) {
            println("ゲームオーバー")
        } else {
            println("リンゴ美味しかったー!")
        }
    }
}


class Human {
    // このメソッドからeat()メソッドを呼び出す. テストコードに相当する部分でもあります.
    fun doSomething() {
        val apple = Apple(hasPoison = false)
        val poisonApple = Apple(hasPoison = true)
        apple.eat()
        poisonApple.eat()
    }
}

DIするメリット

最後に、これまで各所で説明してきたメリットを再度提示します。

  • 結合度の低下によるコンポーネント化の促進
  • 単体テストの効率化
  • 特定のフレームワークへの依存度低下

上記3つのメリットは、Wikipediaより引用。

テストにおけるメリットは頭では理解できますが体感したことはあまりないため、今後実際にテストコードを書いてみる予定です!

まとめ

いかがでしたでしょうか?
今、タイトルを見返すとその意味を少しでも理解していただけたでしょうか?
(あるいは、文字だけを見たときの意味の分からなさを実感していただけたでしょうか?)

今回は DI について学習できたため、次の記事では実際に Dagger2 を導入しその概要をまとめようと考えています。

参考資料

Discussion