第5項:固定資源首選使用依賴注入
許多類依賴於一個或多個底層資源。 例如,拼寫檢查器依賴於字典。常見的做法是將這些類實現為靜態實用程式類(第4項):
// Inappropriate use of static utility - inflexible & untestable! public class SpellChecker { private static final Lexicon dictionary = ...; private SpellChecker() {} // Noninstantiable public static boolean isValid(String word) { ... } public static List<String> suggestions(String typo) { ... } }
同樣的,將它們作為單例實現的情況並不少見(第3項):
// Inappropriate use of singleton - inflexible & untestable! public class SpellChecker { private final Lexicon dictionary = ...; private SpellChecker(...) {} public static INSTANCE = new SpellChecker(...); public boolean isValid(String word) { ... } public List<String> suggestions(String typo) { ... } }
這些方法都不令人滿意,因為它們假設只有一本值得使用的字典。 在實踐中,每種語言都有自己的字典,特殊字典用於特殊詞彙。 而且,可能需要使用特殊字典進行測試。 假設單本字典就足以滿足所有情況,這是一廂情願的想法。
你可以嘗試讓SpellChecker支援多個詞典,方法是使字典欄位為非final域,並新增一個方法來更改現有拼寫檢查器中的字典,但這在併發時設定會很笨拙,容易出錯並且不可行。 靜態實用程式類和單例不適用於底層資源作為引數的類(Static utility classes and singletons are inappropriate for classes whose behavior is parameterized by an underlying resource.)。
所需要的是能夠支援類的多個例項(在我們的示例中為SpellChecker),每個例項都使用客戶端所需的資源(在我們的示例中為字典)。 滿足此要求的簡單模式是在建立新例項時將資源傳遞給建構函式。 這是依賴注入的一種形式:字典是拼寫檢查器的依賴項,並在建立時注入拼寫檢查器。
// Dependency injection provides flexibility and testability public class SpellChecker { private final Lexicon dictionary; public SpellChecker(Lexicon dictionary) { this.dictionary = Objects.requireNonNull(dictionary); } public boolean isValid(String word) { ... } public List<String> suggestions(String typo) { ... } }
這種依賴注入很簡單,以至於程式猿用了很多年卻不知道它有一個名稱。雖然我們的拼寫檢查器只有一個資源(字典),但是依賴注入可以使用任意數量的資源和任意的依賴關係,它保留了不變性(第17項),因此多個客戶端可以共享依賴物件(假設客戶端需要相同的底層資源)。依賴注入同樣適用於建構函式、靜態工廠(第1項)和構建器(第2項)。
將資源工廠傳遞給建構函式就會變成一個有用的模式。工廠是一個物件,通過重複呼叫這個工廠可以建立某個型別的例項物件。這些就是工廠方法模式 [Gamma95]。Java 8中引入的Supplier <T>介面非常適合體現工廠。在輸入上採用Supplier <T>的方法通常應該使用泛型(第31項)約束工廠的型別引數,以允許客戶端傳入建立指定型別的任何子型別的工廠。例如,這是一種使用客戶提供的工廠生成馬賽克來生成每個圖塊的方法:
Mosaic create(Supplier<? extends Tile> tileFactory) { ... }
儘管依賴注入極大地提高了靈活性和可測試性,但它可能會使大型專案更加混亂,這些專案通常包含數千個依賴項。通過使用依賴注入框架,例如Dagger [Dagger],Guice [Guice]或Spring [Spring],可以消除這種混亂。這些框架的使用超出了本書的範圍,但請注意,為手動依賴注入而設計的API可以輕鬆地適用於這些框架。
總之,如果有一個類依賴一個或多個底層資源的類,並且底層資源類影響了類的行為,不要使用單例或靜態實用程式類來實現它,並且不要讓類直接建立這些資源(do not use a singleton or static utility class to implement a class that depends on one or more underlying resources whose behavior affects that of the class)。相反,將資源或工廠傳遞給建構函式(或靜態工廠或構建器)。這種做法稱為依賴注入,將極大地增強類的靈活性,可重用性和可測試性。