很多類依賴于一個(gè)或者多個(gè)相關(guān)資源。比如,一個(gè)拼寫檢測(cè)器依賴一個(gè)字典。這樣的類實(shí)現(xiàn)為靜態(tài)效用類,這是很常見(jiàn)的(條目4):
// 不恰當(dāng)?shù)撵o態(tài)效用的使用 - 不靈活而且不可測(cè)試!
public class SpellChecker {
private static final Lexicon dictionary = ...;
private SpellChecker() {} // 不可實(shí)例化化
public static boolean isValid(String word) { ... }
public static List<String> suggestions(String typo) { ... }
}
相似地,這些類實(shí)現(xiàn)為單例,這也是很常見(jiàn)的(條目3):
// 不恰當(dāng)?shù)膯卫氖褂?- 不靈活而且不可測(cè)試!
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) { ... }
}
這些方法沒(méi)有一個(gè)是令人滿意的,因?yàn)樗麄兗僭O(shè)只有一個(gè)值得使用的字典。在實(shí)踐中,每個(gè)語(yǔ)言有自己的字典,特定的字典使用特定的詞匯。而且,測(cè)試可能有特定的字典。認(rèn)為單個(gè)字典對(duì)所有時(shí)間都?jí)蛴?,這是一廂情愿的想法。
你可能用這種方法讓SpellChecker支持多個(gè)字典:字典的域?yàn)榉莊inal,然后在目前的拼寫檢測(cè)器中添加一個(gè)改變字典的方法。但是這個(gè)可能很笨拙,容易出錯(cuò),而且在多線程環(huán)境不能工作。靜態(tài)效用類和單例對(duì)于這樣的類是不恰當(dāng)?shù)模核男袨槭怯上嚓P(guān)資源參數(shù)化。
要求的是這種能力:支持這個(gè)類的多個(gè)實(shí)例(在我們的例子中,SpellChecker),每個(gè)實(shí)例使用客戶端要求的資源(我們的例子中,字典)。滿足需求的一個(gè)簡(jiǎn)單模式是,當(dāng)創(chuàng)建一個(gè)新實(shí)例時(shí),把資源傳入到構(gòu)造子。這是依賴注入的一個(gè)形式:字典作為拼寫檢測(cè)器的一個(gè)依賴,當(dāng)創(chuàng)建時(shí)它被注入到拼寫檢測(cè)器中。
// 依賴注入具有靈活性和可測(cè)試性
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) { ... }
}
依賴注射是如此簡(jiǎn)單,以致許多程序員多年使用而不知道它的名字。雖然我們的拼寫檢測(cè)器例子僅僅只有單一資源(即,字典),依賴注射對(duì)于任意資源數(shù)量和任意依賴圖譜也起作用。它保持不可變性(條目17),所以多個(gè)客戶端可以分享依賴對(duì)象(假設(shè)客戶端需要同樣的相關(guān)資源)。依賴注射同樣可以應(yīng)用到構(gòu)造子,靜態(tài)工廠(條目1)和builder(條目2)。
這個(gè)模式的一個(gè)有用變體是,把資源工廠傳遞給構(gòu)造子。工廠是一個(gè)對(duì)象,可以重復(fù)調(diào)用創(chuàng)建一種類型的實(shí)例。這樣的工廠具體表現(xiàn)為工廠方法模式(Factory Method)[Gamma95]。Java 8引入的Supplier<T>接口完美代表工廠。輸入有Supplier<T>的方法,往往用受限的通配類型(bounded wildcard type)(條目31)來(lái)限制工廠類型參數(shù),這讓客戶端傳入一個(gè)工廠,這個(gè)工廠可以創(chuàng)建指定類型的任何子類型。比如,這里有個(gè)方法,用客戶端提供的工廠生成每個(gè)地磚來(lái)鋪設(shè)鑲嵌地磚。
Mosaic create(Supplier<? extends Tile> tileFactory) { ... }
盡管依賴注射極大提高了靈活性和可測(cè)試性,但是它使得大項(xiàng)目凌亂,大項(xiàng)目往往包含成千個(gè)依賴。用依賴注射框架(∫dependency injection framework)可以消除凌亂,比如Dagger[Dagger]、Guice[Guice]或者Spring[Spring]。使用這些框架不在這本書的范圍,但是記住,為手動(dòng)依賴注入設(shè)計(jì)的API,可以用這些框架輕松適配。
總之,不要用單例或者靜態(tài)效用類實(shí)現(xiàn)這樣的類,這個(gè)類依賴一個(gè)或者多個(gè)相關(guān)資源,而這些資源的行為影響了這個(gè)類。不要用這個(gè)類直接創(chuàng)建這些資源。相反,傳遞資源或者工廠到構(gòu)造子來(lái)創(chuàng)建它們(或者靜態(tài)工廠或者builder)。這個(gè)實(shí)踐,叫做依賴注射,大大的增強(qiáng)了一個(gè)類的靈活性、重用性和可測(cè)試性。