依附元件插入 (DI) 功能是程式設計中廣泛使用的技術,適用於 Android 開發作業。請按照 DI 原則,為優質應用程式架構奠定基礎。
實作插入依附元件功能具備下列優點:
- 程式碼可重複使用
- 重構輕鬆
- 測試便利
插入依附元件的基礎知識
本頁面會先簡介通用的依附元件插入功能運作方式,再特別說明 Android 中插入依附元件的作業。
什麼是插入依附元件?
類別通常需要參照其他類別。舉例來說,Car
類別可能需要參照 Engine
類別。這些必要類別稱為「依附元件」,在這個範例中,Car
類別依附於要執行的 Engine
類別例項。
類別有三種方法可以取得所需的物件:
- 類別會建構所需的依附元件。在上述範例中,
Car
會建立並初始化其專屬的Engine
例項。 - 從其他位置取得。部分 Android API (例如
Context
getter 和getSystemService()
) 會以這種方式運作。 - 以參數的形式提供。應用程式可以在建構類別時提供這些依附元件,或傳遞至需要各依附元件的函式。在上述範例中,
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(); } }
![沒有插入依附元件的車輛類別](https://developer.android.com/static/images/training/dependency-injection/1-car-engine-no-di.png?hl=zh-tw)
這並不是插入依附元件的範例,因為 Car
類別正在建構自己的 Engine
。這可能會造成問題,原因如下:
Car
和Engine
緊耦合:Car
的例項使用一種Engine
類型,而且沒有可以輕鬆運用的子類別或替代實作項目。如果Car
要自行建構Engine
,您必須建立兩種類型的Car
,而不是針對Gas
和Electric
類型的引擎重複使用相同的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(); } }
![使用插入依附元件的車輛類別](https://developer.android.com/static/images/training/dependency-injection/1-car-engine-di.png?hl=zh-tw)
main
函式使用 Car
。由於 Car
依附 Engine
,因此應用程式會建立 Engine
的例項,然後用來建構 Car
的例項。這種以 DI 為基礎的方法優點如下:
Car
可重複使用。您可以將不同的Engine
實作傳遞至Car
。例如,您可以定義要讓Car
使用的新Engine
子類別 (名為ElectricEngine
)。如果您使用 DI,只需傳入更新後的ElectricEngine
子類別的例項,Car
仍能自動運作,無須進行任何變更。測試
Car
十分簡單。您可以傳遞測試替身,以便測試不同情境。舉例來說,您可以建立一個名為FakeEngine
的Engine
測試替身,並根據不同的測試進行設定。
在 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 介面的可驗證部分,因此您可以在物件建立時間或編譯時間查看,而不必被隱藏為實作詳細資料。
易於測試:類別不會管理其依附元件,因此在測試時,您可以傳遞不同的實作項目來測試各種不同情況。
要完整瞭解依附元件插入的優點,建議您在應用程式中手動導入,如「手動插入依附元件」一節所述。
其他資源
如要進一步瞭解依附元件插入功能 歡迎參考下列其他資源。