依存関係インジェクション(DI)はプログラミングで広く使用されている手法で、Android 開発にも適しています。DI の原則に従うことで、優れたアプリ アーキテクチャの土台を築くことができます。
依存関係インジェクションを実装すると、次のようなメリットがもたらされます。
- コードを再利用できる
- リファクタリングが容易になる
- テストが容易になる
依存関係インジェクションの基礎
このページでは、Android での依存関係インジェクションについて具体的に説明する前に、依存関係インジェクションの大まかな仕組みを示します。
依存関係インジェクションとは
多くの場合、クラスは他のクラスへの参照を必要とします。たとえば、Car
クラスが Engine
クラスへの参照を必要とする場合、このようなクラスの関係を「依存関係」と呼びます。この例では、Car
クラスは、実行に必要な Engine
クラスのインスタンスに依存します。
クラスが必要なオブジェクトを取得するには、次の 3 つの方法があります。
- クラス自身が必要な依存関係を構築する。上記の例では、
Car
はEngine
のインスタンスを独自に作成して初期化します。 - 別の場所から入手する。一部の Android API(
Context
ゲッターやgetSystemService()
など)は、この方法で機能します。 - パラメータとして受け取る。アプリを通じて、クラスの構築時に依存関係を提供したり、依存関係を必要とする関数に個別に渡したりできます。上記の例では、
Car
コンストラクタはパラメータとしてEngine
を受け取ります。
3 つ目の方法が依存関係インジェクションです。このアプローチでは、クラス インスタンスが独自に依存関係を取得するのではなく、アプリでクラスの依存関係を取得してクラスに渡します。
次の例をご覧ください。このコードは、依存関係インジェクションを行わないで独自に Engine
依存関係を作成する Car
を表しています。
Kotlin
class Car { private val engine = Engine() fun start() { engine.start() } } fun main(args: Array) { val car = Car() car.start() }
Java
class Car { private Engine engine = new Engine(); public void start() { engine.start(); } } class MyApp { public static void main(String[] args) { Car car = new Car(); car.start(); } }
これは依存関係インジェクションの例ではありません。Car
クラスは独自の Engine
を構築しているからです。この方法には次のような問題があります。
Car
とEngine
が緊密に結び付けられます。つまり、Car
のインスタンスは 1 種類のEngine
を使用し、サブクラスや代替実装を簡単に使用することができません。Car
が独自のEngine
を構築する場合、Gas
タイプとElectric
タイプのエンジンについて同じCar
を再利用する代わりに、2 つのタイプのCar
を作成する必要があります。Engine
への依存関係が固定されていると、テストが困難になります。Car
はEngine
の実際のインスタンスを使用するので、テストダブルを使用して別のテストケース用にEngine
を変更することができません。
依存関係インジェクションを行うコードがどのように動作するかを見てみましょう。Car
の各インスタンスが初期化時に独自の Engine
オブジェクトを構築するのではなく、Engine
オブジェクトをコンストラクタ内でパラメータとして受け取ります。
Kotlin
class Car(private val engine: Engine) { fun start() { engine.start() } } fun main(args: Array) { val engine = Engine() val car = Car(engine) car.start() }
Java
class Car { private final Engine engine; public Car(Engine engine) { this.engine = engine; } public void start() { engine.start(); } } class MyApp { public static void main(String[] args) { Engine engine = new Engine(); Car car = new Car(engine); car.start(); } }
main
関数は Car
を使用します。Car
は Engine
に依存するため、アプリは Engine
のインスタンスを作成し、それを使用して Car
のインスタンスを構築します。この DI ベースのアプローチには、次の利点があります。
Car
を再利用できる。Engine
のさまざまな実装をCar
に渡すことができます。たとえば、Car
が使用するEngine
の新しいサブクラスをElectricEngine
という名前で定義できます。DI を使用する場合、必要なのは更新されたElectricEngine
サブクラスのインスタンスを渡すことだけです。Car
はそれ以上の変更なしで機能します。Car
のテストが容易になる。テストダブルを導入して、さまざまなシナリオをテストできます。たとえば、Engine
のテストダブルをFakeEngine
という名前で作成し、さまざまなテスト用に構成できます。
Android で依存関係インジェクションを行う主な方法は 2 つあります。
コンストラクタ インジェクション。上記の方法です。クラスの依存関係をコンストラクタに渡します。
フィールド インジェクション(またはセッター インジェクション)。アクティビティやフラグメントなど、一部の Android フレームワーク クラスはシステムによってインスタンス化されるため、コンストラクタ インジェクションは不可能です。フィールド インジェクションでは、クラスの作成後に依存関係がインスタンス化されます。次のようなコードを使用します。
Kotlin
class Car { lateinit var engine: Engine fun start() { engine.start() } } fun main(args: Array) { val car = Car() car.engine = Engine() car.start() }
Java
class Car { private Engine engine; public void setEngine(Engine engine) { this.engine = engine; } public void start() { engine.start(); } } class MyApp { public static void main(String[] args) { Car car = new Car(); car.setEngine(new Engine()); car.start(); } }
自動の依存関係インジェクション
上記の例では、ライブラリに依存せずに、さまざまなクラスの依存関係を独自に作成、提供、管理しています。この方法は、手動の依存関係インジェクション(手動 DI)と呼ばれます。Car
の例では依存関係は 1 つのみでしたが、依存関係とクラスの数が増えると、手動の依存関係インジェクションは面倒な作業になります。手動の依存関係インジェクションには、他にも問題がいくつかあります。
大規模なアプリでは、すべての依存関係を取得して正しく接続するために、大量のボイラープレート コードが必要になることがあります。マルチレイヤ アーキテクチャでは、最上位レイヤのオブジェクトを作成するために、その下のレイヤの依存関係をすべて指定する必要があります。たとえば、実際の自動車を組み立てるには、エンジン、トランスミッション、シャーシ、その他の部品が必要です。さらに、エンジンにはシリンダーと点火プラグが必要です。
遅延初期化を使用する場合やオブジェクトのスコープをアプリのフローに設定する場合など、依存関係を渡す前に依存関係を構築できない場合は、メモリ内で依存関係のライフタイムを管理するカスタム コンテナ(または依存関係のグラフ)を作成および管理する必要があります。
この問題は、依存関係の作成と提供のプロセスを自動化するライブラリにより解決できます。それには次の 2 種類の方法があります。
実行時に依存関係を接続するリフレクション ベースのソリューション。
コンパイル時に依存関係を接続するコードを生成する静的ソリューション。
Dagger は、Google が管理する Java、Kotlin、Android で広く使用されている依存関係インジェクション ライブラリです。Dagger は、アプリ用の依存関係のグラフを作成して管理することで、アプリでの DI の利用を促進します。完全に静的な依存関係をコンパイル時に提供することにより、Guice のようなリフレクション ベースのソリューションで発生する開発とパフォーマンスに関する問題の多くを解決できます。
依存関係インジェクションの代替手段
依存関係インジェクションの代替手段として、サービス ロケータがあります。サービス ロケータ設計パターンでは、実際の依存関係からクラスをより確実に分離することができます。サービス ロケータと呼ばれるクラスを作成すると、依存関係を作成して保存したうえで、その依存関係をオンデマンドで提供できます。
Kotlin
object ServiceLocator { fun getEngine(): Engine = Engine() } class Car { private val engine = ServiceLocator.getEngine() fun start() { engine.start() } } fun main(args: Array) { val car = Car() car.start() }
Java
class ServiceLocator { private static ServiceLocator instance = null; private ServiceLocator() {} public static ServiceLocator getInstance() { if (instance == null) { synchronized(ServiceLocator.class) { instance = new ServiceLocator(); } } return instance; } public Engine getEngine() { return new Engine(); } } class Car { private Engine engine = ServiceLocator.getInstance().getEngine(); public void start() { engine.start(); } } class MyApp { public static void main(String[] args) { Car car = new Car(); car.start(); } }
サービス ロケータ パターンは、要素の使用方法の点で依存関係インジェクションと異なります。サービス ロケータ パターンでは、注入されるオブジェクトをクラスが制御し、注入を要求します。依存関係インジェクションでは、必要なオブジェクトをアプリが制御し、事前に注入します。
依存関係インジェクションとの比較:
サービス ロケータでは依存関係のコレクションが必要となり、すべてのテストが同じグローバル サービス ロケータとやり取りする必要があるため、コードのテストが困難になります。
依存関係は、API サーフェスではなくクラス実装にエンコードされます。そのため、クラスが外部に必要とするものを把握することが困難です。その結果、
Car
またはサービス ロケータで利用可能な依存関係を変更すると、参照が失敗してランタイム エラーまたはテストエラーが発生する可能性があります。アプリ全体のライフタイム以外の期間にスコープを設定したい場合、オブジェクトのライフタイムの管理が難しくなります。
Android アプリでは Hilt を使用する
Hilt は、Android で依存関係インジェクションを行うための Jetpack の推奨ライブラリです。Hilt は、プロジェクト内のすべての Android クラスにコンテナを提供し、そのライフサイクルを自動で管理することで、アプリケーションで DI を行うための標準的な方法を定義します。
Hilt は、よく知られた DI ライブラリである Dagger の上に構築されているため、コンパイル時の正確性、実行時のパフォーマンス、スケーラビリティ、Android Studio のサポートといった Dagger の恩恵を受けられます。
Hilt の詳細については、Hilt を使用した依存関係インジェクションをご覧ください。
まとめ
依存関係インジェクションには次のような利点があります。
クラスの再利用と依存関係の分離: 依存関係の実装を簡単に切り替えられます。制御の反転により、コードの再利用性が向上します。クラスで依存関係の作成方法を管理する必要がなくなり、どのような構成でも機能します。
リファクタリングの容易さ: 依存関係が、実装の詳細部分として見えなくなるのではなく、API サーフェスの検証可能な部分に組み込まれるため、オブジェクトの作成時またはコンパイル時に依存関係をチェックできます。
テストの容易さ: クラスで依存関係を管理しないため、テストの際には複数の実装を用意して、さまざまなケースを検証できます。
依存関係インジェクションの利点を十分に理解するには、手動の依存関係インジェクションで説明している手動 DI を実際のアプリで試してみてください。
参考情報
依存関係インジェクションの詳細については、以下の参考情報をご覧ください。