在 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 介面的可驗證部分,因此您可以在物件建立時間或編譯時間查看,而不必被隱藏為實作詳細資料。

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

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

其他資源

如要進一步瞭解依附元件插入功能 歡迎參考下列其他資源。

範例