在 Android 中插入依附元件

插入依附元件 (DI) 是程式設計中廣泛使用的技術,適用於 Android 開發作業。請按照 DI 的原則,為優質應用程式架構奠定基礎。

實作插入依附元件功能具備下列優點:

  • 程式碼可重複使用
  • 重構輕鬆
  • 測試便利

插入依附元件基礎知識

確定在 Android 中插入依附元件前,本頁面將提供更多依附元件插入功能的運作方式總覽。

什麼是插入依附元件?

類別通常需要參照其他類別。舉例來說,Car 類別可能需要參照 Engine 類別。這些必要類別稱為「依附元件」,在這個範例中,Car 類別依附於要執行的 Engine 類別執行個體。

類別有三種方法可以取得所需的物件:

  1. 類別會建構所需的依附元件。在上述範例中,Car 會建立並初始化其專屬的 Engine 執行個體。
  2. 從其他位置取得。部分 Android API (例如 Context getter 和 getSystemService()) 會以這種方式運作。
  3. 以參數的形式提供。應用程式可以在建構類別時提供這些依附元件,或傳遞至需要每個依附元件的函式。在上述範例中,Car 建構函式會收到 Engine 做為參數。

第三個選項是插入依附元件!這個方法使用類別的依附元件並提供類別,而不是讓類別執行個體自行取得。

以下將舉例說明。如果沒有依附元件插入,代表 Car 會在程式碼中建立其自己的 Engine 依附元件,如下所示:

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。此問題的可能原因如下:

  • CarEngine 緊密耦合 - Car 的執行個體使用一種 Engine 類型,而且沒有子類別或替代實作可以使用。如果 Car 要自行建構 Engine,您必須建立兩種類型的 Car,而不是重複使用 GasElectric 類型的 Car 引擎。

  • Engine 的硬性依附元件讓測試變得更困難。Car 使用 Engine 的實際執行個體,因此防止您使用測試替身修改不同測試案例的 Engine

使用依附元件插入時,程式碼會是什麼樣子?每個執行個體在建構函式中接收 Engine 物件做為參數,而不是在 Car 的每個執行個體在初始化時建構自己的 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 的輕鬆測試。您可以傳入測試替身,以測試不同的情境。舉例來說,您可以建立一個名為 FakeEngineEngine 測試替身,並針對不同的測試對其進行設定。

在 Android 中的執行依附元件插入有兩種方法:

  • 建構函式建構函式。這就是上述的方式。您將類別的依附元件傳遞至其建構函式。

  • 欄位插入 (或 Setter 插入)。特定 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();
    }
}

自動插入依附元件

在先前的範例中,您已自行建立、提供及管理不同類別的依附元件,不必依賴程式庫。這就是所謂的「手動插入依附元件」或「手動依附元件插入」。在 Car 範例中,只有一個依附元件,但更多的依附元件和類別可以讓手動插入依附元件更加繁瑣。手動依附元件插入也有幾個問題:

  • 針對大型應用程式,擷取所有依附元件並正確連結,可能需要大量的樣板程式碼。在多層架構中,如要為頂層建立物件,您必須提供其下方圖層的所有依附元件。以具體範例來說,如要建構真正的車輛,您可能需要引擎、變速箱、底盤和其他零件;而引擎則需要使用氣瓶和火花塞。

  • 如果您無法在將其傳入之前建構依附元件 (例如,使用延遲初始化或物件範圍限定至應用程式流程時),就必須編寫及維護自訂容器 (或依附元件圖形),因其管理記憶體中依附元件的生命週期。

部分程式庫會自動化建立程序及提供依附元件,藉此解決這個問題。可與兩種類別相容:

  • 在執行階段中連結依附元件的反射性解決方案。

  • 在編譯時會產生程式碼以連結依附元件的靜態解決方案。

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 是 Jetpack 建議在 Android 依附元件中安裝的程式庫。Hilt 為專案中的每個 Android 類別提供容器,並自動管理其生命週期,藉此定義在應用程式中執行 DI 的標準方法。

Hilt 以熱門的 DI 程式庫 Dagger 為基礎建構而成,並享有 Dagger 提供的編譯時間正確性、執行階段效能、擴充性和 Android Studio 支援。

如要進一步瞭解 Hilt,請參閱「使用 Hilt 插入依附元件」。

結語

依附元件插入功能可為應用程式提供下列優點:

  • 可重複使用類別和分離依附元件:較容易取代依附元件的實作。因為控制反轉,改善了程式碼重複使用作業,類別也無法再控制其依附元件的建立方式,但可與任何設定搭配使用。

  • 重構輕鬆:依附元件會成為 API 介面的可驗證部分,因此您可以在物件建立時間或編譯時間查看,而不必被隱藏為實作詳細資料。

  • 測試難易度:類別不會管理其依附元件,因此在測試時,您可以傳遞不同的實作項目來測試各種不同情況。

要完整瞭解依附元件插入的優點,建議您在應用程式中手動導入,如「手動插入依附元件」一節中所述。

其他資源

如要進一步瞭解插入依附元件,請參閱下列其他資源。

範例