DIに入門してみる

なぜ入門するのか

DI という言葉だけは知っているけど実際に導入したことはない。
DI が何なのかよくわかっていない。メリットがよくわからない。
最近、業務で Android アプリを作ることになった。Android アプリの開発ではよく DI が使われている気がする。それなのに DI について知らないのはモヤモヤするので、DI について少しでも理解したい。
あとせっかくブログを立ち上げたのに書くネタがないw

※ 学生時代に Android アプリを作った経験はあるけど、DI なんて使っていないし、なんならすべてのフォアグラウンド処理を Activity に書いてたくらいガバガバな作り方をしていた。

DI についてのイメージ

入門前

依存性?を注入?するらしい。それの何が嬉しいの?
「DI」で調べるとライブラリを使ったコードがいっぱい出てくる。ライブラリ使わないとできないんじゃないの?

入門後

DI は開発・テストを実施しやすくするためにクラス同士を疎結合にするための設計手法だよ。
ライブラリを使わなくても DI できるよ。

DI について

DI とは依存関係を外部から注入する手法である。
これだけ聞いてもよく分からないので、例を見せながら説明する。

DI を意識していない設計

コードはAndroid Developer Documentationに書いてあるものを参考にしている。

以下は DI を意識していないコードである。
Carクラスの中でEngineクラスのインスタンスを生成している。Carクラスのstart()メソッドが呼ばれると、Engineクラスで定義されているstart()メソッドが呼び出される。

class Car {

    private val engine = Engine()

    fun start() {
        engine.start()
    }
}

上記の設計ではCarクラス内で使っているEngineクラスを差し替えることができない。
差し替えることができないと以下のような問題が発生する。

  1. Carクラスを再利用できない。ガソリン車・電気自動車を表すクラスを用意する必要があるとき、GasEngineElectric Engineクラスを作成する他に、GasCarElectricCarクラスも作成する必要がある。
  2. Carクラスのユニットテストができない。Engineをテスト用のモッククラスに差し替えてテストを行うことができない。

…問題点はわかったけど、サンプルコードレベルの単純な処理だといまいちピンとこないので、実際の Android アプリで使われがちなシーンに置き換えてみる。

例:
HogeViewModel から FugaRepository を呼び出す。
FugaReposiory では、API サーバと通信してレスポンスを取得するための処理が書かれている。
HogeViewModel では、取得したレスポンスを元に処理を色々行う。レスポンスの内容によって行われる処理は異なる。

class HogeViewModel: ViewModel() {
    fun foo() {
        val fuga = FugaRepository()
        val response = fuga.getData()

        // 以降、受け取ったレスポンスを元に色々処理を行う
        // レスポンスの内容によって行われる処理は異なる
    }
}
class FugaRepository {
    fun getData(): PiyoResponse {
        // APIサーバと通信してデータを取得して返す
    }
}

※ 本来であれば Coroutine とか使わないといけないけどわかりやすくするために省略。

ここで ViewMdel のテストを実施したいとする。
HogeViewMdel#foo()は、API サーバのレスポンスの内容によって行われる処理が変わるため、FugaReposiory をそのまま使っている上記の設計では、HogeViewMdel#foo()内で実行されるすべての処理パターンをテストすることが困難になる。
テストが実施できないと困るよね。DI を意識していない設計ではテストが書きにくい。

DI を意識した設計

先程のCarEngineクラスを DI を意識した設計に置き換えてみる。 DI を行う主な方法は 2 つある。

  1. コンストラクタインジェクション
  2. フィールドインジェクション(またはセッターインジェクション)

コンストラクタインジェクション

コンストラクタインジェクションは、その名の通り、コンストラクタで依存性を注入する方法。 先程のCarEngineクラスにコンストラクタインジェクションを導入すると以下のようになる。

class Car (private val engine: Engine) {

    fun start() {
        engine.start()
    }
}

interface Engine {
    fun start()
}

class GasEngine(): Engine {
    override fun start() {
        // 省略
    }
}

呼び出しは以下のような感じ。

val engine = GasEngine()
val car = Car(engine)
car.start()

フィールドインジェクション(またはセッターインジェクション)

フィールドインジェクション(またはセッターインジェクション)では先程のCarEngineクラスは以下のようになる。 コンストラクタではなく、フィールドに対して Engine のインスタンスをセットしている。

class Car {
    lateinit var engine: Engine

    fun start() {
        engine.start()
    }
}

interface Engine {
    fun start()
}

class GasEngine(): Engine {
    override fun start() {
        // 省略
    }
}

呼び出しは以下のような感じ。

val car = Car()
car.engine = GasEngine()
car.start()

どちらを使えばいいのか

Android Developer Documentationには以下のような記述がある。

アクティビティやフラグメントなど、一部の Android フレームワーク クラスはシステムによってインスタンス化されるため、コンストラクタ インジェクションは不可能です。

コンストラクタを使って DI するのも、フィールドを使って DI するのも、やれることは変わらないけど、Android 開発においては場所によって採用する方法が変わるみたい。

ServiceLocator

DI の代替手段。要は、ServiceLocator クラス(別クラス)に依存関係をリスト化して持たせておく手法。

object ServiceLocator {
    fun getEngine() = GasEngine()
}


class Car {
    private val engine: Engine = ServiceLocator.getEngine()

    fun start() {
        engine.start()
    }
}

interface Engine {
    fun start()
}

class GasEngine(): Engine {
    override fun start() {
        // 省略
    }
}

呼び出しは以下のような感じ。

val car = Car()
car.start()

非 DI な設計と DI(または ServiceLocator)な設計を比べると…

DI(または ServiceLocator)では、どの手法でもCarEngineが疎結合になっている。
これにより、Engineクラスを別のクラスに差し替えることができる。
差し替えることで、新規にElectricEngine(EV エンジン)クラスを作成したとしても既存のCarクラスを再利用できるし、Carクラスのテスト時に、テスト用のTestEngineクラスに差し替えてテストを実施できる。
DI することで、開発・テストがしやすくなるね!

参考

Android Developer Documentation > Android での依存関係インジェクション
DI (依存性注入) って何のためにするのかわからない人向けに頑張って説明してみる

次回

次回は Android 開発でよく使われる DI ライブラリ「Hilt」に入門してみる。