閱讀151 返回首頁    go 京東網上商城


靜態工廠方法VS構造器

我之前已經介紹過關於構建者模式 (Builder Pattern)的一些內容,它是一種很有用的模式用於實例化包含幾個屬性(可選的)的類,帶來的好處是更容易讀、寫及維護客戶端代碼。今天,我將繼續介紹對象創建技術。
在我看來,下麵這個類是非常有用的例子。有一個RandomIntGenerator 類,產生隨機的int類型的整數。如下所示:

1 public class RandomIntGenerator {
2 private final int min;
3 private final int max;
4  
5 public int next() {...}
6 }

這個生成器接收最大值和最小值兩個參數並且生成介於兩者之間的隨機數。注意到兩個屬性min和max被final修飾,所以必須初始化它們。可以在定義它們時就初始化或者通過構造器來初始化。通過構造器初始如下:

1 public RandomIntGenerator(int min, int max) {
2     this.min = min;
3     this.max = max;
4 }

現在,我們要提供給這樣一個功能,客戶端僅設置一個最小值然後產生一個介於此值和Integer.MAX_VALUE之間的整數。所以我們添加了第二個構造器:

1 public RandomIntGenerator(int min) {
2     this.min = min;
3     this.max = Integer.MAX_VALUE;
4 }

到目前為止,一切正常。但是,我們同樣要提供一個構造器設置一個最大值,然後產生一個介於Integer.MIN_VALUE和最大值的整數。我們添加第三個構造器如下:

1 public RandomIntGenerator(int max) {
2     this.min = Integer.MIN_VALUE;
3     this.max = max;
4 }

如果你這樣做了,將會出現編譯錯誤:Duplicate method RandomIntGenerator(int) in type RandomIntGenerator。那裏出錯了?毫無疑問,問題在於構造器沒有名字。因此,一個類僅有一個特定方法簽名的構造器。同樣的,你不能定義方法簽名相同的(返回值、名稱、參數類型及個數均相同)兩個方法。這就是為什麼當我們試著添加構造器RandomIntGenerator(int max) 時,會得到上述的編譯錯誤,原因是我們已經有一個構造器 RandomIntGenerator(int min)。

像這樣的情況我們該如何處理呢?幸好有其他方式可以使用:靜態工廠方法,通過使用簡單的公共靜態方法返回一個類的實例。你可能在無意識中已經使用過這種技術。你有沒有寫過Boolean.valueOf?,就像下麵這樣:

1 public static Boolean valueOf(boolean b) {
2     return (b ? TRUE : FALSE);
3 }

將靜態工廠應用到RandomIntGenerator類,得到

01 public class RandomIntGenerator {
02     private final int min;
03     private final int max;
04  
05     private RandomIntGenerator(int min, int max) {
06         this.min = min;
07         this.max = max;
08     }
09  
10     public static RandomIntGenerator between(int max, int min) {
11         return new RandomIntGenerator(min, max);
12     }
13  
14     public static RandomIntGenerator biggerThan(int min) {
15         return new RandomIntGenerator(min, Integer.MAX_VALUE);
16     }
17  
18     public static RandomIntGenerator smallerThan(int max) {
19         return new RandomIntGenerator(Integer.MIN_VALUE, max);
20     }
21  
22     public int next() {...}
23 }

注意到構造器被private修飾確保類僅能通過靜態工廠方法來初始化。並且當你使用RandomIntGenerator.between(10,20)而不是new RandomIntGenerator(10,20)來產生整數時你的意圖被清晰的表達了。值得注意的是,這個技術和 Gang of Four的工廠設計模式不同。此外,任何類可以提供靜態工廠方法替代構造器。那麼此種技術的優點和缺點是什麼呢?我們已經提到過靜態工廠方法的第一個優點:靜態工廠方法擁有名字。這有兩個直接的好處:
1.我們可以給靜態方法提供一個有意義的名字
2.我們可以給靜態方法提供參數類型、參數個數相同的幾個構造器,在傳統構造器中是不能這樣做的
另一個優點是:不像構造器,靜態工廠方法不需要每次在調用時創建一個新的對象。當使用不可變類(immutable class)產生常量對象用於常用值且避免了不必要的重複對象是非常有用的。上麵的列子Boolean.valueOf完美的表明了這一點。注意到這個靜態方法返回均為不可變Boolean對象TRUE或者FALSE。
第三個優點是靜態工廠方法的返回對象的類型可以是返回類型的任意子類型。這使你隨意更改返回類型而不用擔心影響到客戶端代碼成為了可能。此外,你可以隱藏實現類並且構建基於接口的API(Interface-based API)。通過一個例子來說明。
記得剛開始的RandomIntGenerator類嗎?我們讓他複雜一點。假設我們現在想提供不僅能產生整型,而且能產生其他數據類型如String, Double或者Long的隨機生成器。這些生成器將有一個next()方法返回一個特定類型的隨機對象,所以我們可以先定義一個接口如:

1 public interface RandomGenerator<T> {
2     T next();
3 }

RandomIntGenerator 的第一個實現類如下:

01 class RandomIntGenerator implements RandomGenerator<Integer> {
02     private final int min;
03     private final int max;
04  
05     RandomIntGenerator(int min, int max) {
06         this.min = min;
07         this.max = max;
08     }
09  
10     public Integer next() {...}
11 }

String類型的生成器如:

1 class RandomStringGenerator implements RandomGenerator<String> {
2     private final String prefix;
3  
4     RandomStringGenerator(String prefix) {
5         this.prefix = prefix;
6     }
7  
8     public String next() {...}
9 }

注意到所有的類及類的構造器被定義為包私有範圍(默認的可見範圍)。這意味著除本包之外的客戶端代碼無法創建這些生成器的實例。那我們該怎麼辦?提示:它以“static”開始,以“methods”結束。考慮下麵這個類:

01 public final class RandomGenerators {
02     // Suppresses default constructor, ensuring non-instantiability.
03     private RandomGenerators() {}
04  
05     public static final RandomGenerator<Integer> getIntGenerator() {
06         return new RandomIntGenerator(Integer.MIN_VALUE, Integer.MAX_VALUE);
07     }
08  
09     public static final RandomGenerator<String> getStringGenerator() {
10         return new RandomStringGenerator('');
11     }
12 }

RandomGenerators類成為了一個不可實例化的工具類,它與靜態工廠方法沒有什麼區別。在同一個包中,不同的生成器類可以高效的獲取和實例化這些類。但是,有意思的部分出現了。注意到這些方法僅返回RandomGenerator 接口,這才是客戶端代碼真正需要的。如果它獲得一個RandomGenerator它就知道調用next()然後得到一個隨機的整數。
假設下月我們編寫了一個新的高效的整數生成器。隻要讓這個新類實現了RandomGenerator我們更換靜態方法中的返回類型,那麼所有客戶端代碼神奇的使用了新的實現。
像RandomGenerators 的類在JDK及第三方類庫是相當常見的。你可以在Collections(java.util)及Lists, Sets or Maps( in Guava)看到許多例子。命名習慣是一樣的:如果你有一個接口命名為Type,那麼在不可實例化類中的靜態方法名就是Types。
最後一個優點是靜態工廠是實例化參數類很簡潔。你曾經見過像這樣的代碼嗎?

1 Map<String, List<String>> map = new HashMap<String, List<String>>();

你在代碼的同一行重複相同的參數兩次!如果右邊的賦值可以從左邊被推斷出來,那將是很優雅的做法。使用靜態方法就可以。下麵的代碼取自 Guava’s Maps 類:

1 public static <K, V> HashMap<K, V> newHashMap() {
2   return new HashMap<K, V>();
3 }

所以現在我們的客戶端代碼變成如下:

1 Map<String, List<String>> map = Maps.newHashMap();

相當的優雅,對嗎?這樣的能力被成為類型推斷(Type interface)。值得一提的是Java7通過使用方括號運算符(diamond operator)引入了類型推斷。所以,如果你使用Java7你可以前麵的例子如:

1 Map<String, List<String>> map = new HashMap<>();

靜態工廠的主要缺點是類中沒有public或者protected的構造器無法被繼承。但事實上,在某種情況下這個一件好事,因為鼓勵開發者優先使用組合而不是繼承(favor composition over inheritance)。
總的來講,靜態工廠方法提供了許多優點,當你在使用時唯一的不足之處實際上也不會是一個問題。所以,評估一下你的類是否更適合靜態工廠,拒絕急切的自動提供公共的構造器。

最後更新:2017-05-22 13:01:27

  上一篇:go  《Groovy官方文檔》1.3 Groovy和Java比較
  下一篇:go  特寫 | 人工智能背後的臨時工