Android での依存関係インジェクション

依存関係インジェクション(DI)はプログラミングで広く使用されている手法で、Android 開発にも適しています。DI の原則に従うことで、優れたアプリ アーキテクチャの土台を築くことができます。

依存関係インジェクションを実装すると、次のようなメリットがもたらされます。

  • コードを再利用できる
  • リファクタリングが容易になる
  • テストが容易になる

依存関係インジェクションの基礎

このページでは、Android での依存関係インジェクションについて具体的に説明する前に、依存関係インジェクションの大まかな仕組みを示します。

依存関係インジェクションとは

多くの場合、クラスは他のクラスへの参照を必要とします。たとえば、Car クラスが Engine クラスへの参照を必要とする場合、このようなクラスの関係を「依存関係」と呼びます。この例では、Car クラスは、実行に必要な Engine クラスのインスタンスに依存します。

クラスが必要なオブジェクトを取得するには、次の 3 つの方法があります。

  1. クラス自身が必要な依存関係を構築する。上記の例では、CarEngine のインスタンスを独自に作成して初期化します。
  2. 別の場所から入手する。一部の Android API(Context ゲッターや getSystemService() など)は、この方法で機能します。
  3. パラメータとして受け取る。アプリを通じて、クラスの構築時に依存関係を提供したり、依存関係を必要とする関数に個別に渡したりできます。上記の例では、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 クラス

これは依存関係インジェクションの例ではありません。Car クラスは独自の Engine を構築しているからです。この方法には次のような問題があります。

  • CarEngine が緊密に結び付けられます。つまり、Car のインスタンスは 1 種類の Engine を使用し、サブクラスや代替実装を簡単に使用することができません。Car が独自の Engine を構築する場合、Gas タイプと Electric タイプのエンジンについて同じ Car を再利用する代わりに、2 つのタイプの Car を作成する必要があります。

  • Engine への依存関係が固定されていると、テストが困難になります。CarEngine の実際のインスタンスを使用するので、テストダブルを使用して別のテストケース用に 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();
    }
}
依存関係インジェクションを使用する Car クラス

main 関数は Car を使用します。CarEngine に依存するため、アプリは 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 を実際のアプリで試してみてください。

参考情報

依存関係インジェクション(依存性の注入 - Wikipedia)