DI という言葉だけは知っているけど実際に導入したことはない。
DI が何なのかよくわかっていない。メリットがよくわからない。
最近、業務で Android アプリを作ることになった。Android アプリの開発ではよく DI が使われている気がする。それなのに DI について知らないのはモヤモヤするので、DI について少しでも理解したい。
あとせっかくブログを立ち上げたのに書くネタがないw
※ 学生時代に Android アプリを作った経験はあるけど、DI なんて使っていないし、なんならすべてのフォアグラウンド処理を Activity に書いてたくらいガバガバな作り方をしていた。
依存性?を注入?するらしい。それの何が嬉しいの?
「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
クラスを差し替えることができない。
差し替えることができないと以下のような問題が発生する。
Car
クラスを再利用できない。ガソリン車・電気自動車を表すクラスを用意する必要があるとき、GasEngine
・Electric Engine
クラスを作成する他に、GasCar
・ElectricCar
クラスも作成する必要がある。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 を意識していない設計ではテストが書きにくい。
先程のCar
・Engine
クラスを DI を意識した設計に置き換えてみる。
DI を行う主な方法は 2 つある。
コンストラクタインジェクションは、その名の通り、コンストラクタで依存性を注入する方法。
先程のCar
・Engine
クラスにコンストラクタインジェクションを導入すると以下のようになる。
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()
フィールドインジェクション(またはセッターインジェクション)では先程のCar
・Engine
クラスは以下のようになる。
コンストラクタではなく、フィールドに対して 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 開発においては場所によって採用する方法が変わるみたい。
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(または ServiceLocator)では、どの手法でもCar
とEngine
が疎結合になっている。
これにより、Engine
クラスを別のクラスに差し替えることができる。
差し替えることで、新規にElectricEngine
(EV エンジン)クラスを作成したとしても既存のCar
クラスを再利用できるし、Car
クラスのテスト時に、テスト用のTestEngine
クラスに差し替えてテストを実施できる。
DI することで、開発・テストがしやすくなるね!
Android Developer Documentation > Android での依存関係インジェクション
DI (依存性注入) って何のためにするのかわからない人向けに頑張って説明してみる
次回は Android 開発でよく使われる DI ライブラリ「Hilt
」に入門してみる。