這是Effective Java3中的第二個建議,非常適合初學者寫出簡潔有效的代碼。

靜態工廠和構造方法都有一個缺點:當有很多可選參數的時候,其擴展性並不是很好。例如,考慮這樣一個類,它表示食物包裝上的營養物質標籤。這些標籤有一部分是必須的字段——例如分量大小、每個包裝容器包含的分量大小、每份物質包含的卡路里等,還有一部分是可選字段——例如總的脂肪含量、飽和脂肪含量、反式脂肪含量等等。大多數食品只有一小部分字段是非零的結果。

對於這樣一個類,要如何使用構造方法或者是靜態工廠方法呢?傳統上,編程者可以使用重疊構造函數模式(telescoping constructor pattern),即在某個構造方法中只包含必須的字段,然後添加其他的構造方法包含其他可選字段。舉個例子:假設只有4個可選字段:

public class NutritionFacts { private final int servingSize; // (mL) required private final int servings; // (per container) required private final int calories; // (per serving) optional private final int fat; // (g/serving) optional private final int sodium; // (mg/serving) optional private final int carbohydrate; // (g/serving) optional public NutritionFacts(int servingSize, int servings) { this(servingSize, servings, 0); } public NutritionFacts(int servingSize, int servings, int calories) { this(servingSize, servings, calories, 0); } public NutritionFacts(int servingSize, int servings, int calories, int fat) { this(servingSize, servings, calories, fat, 0); } public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) { this(servingSize, servings, calories, fat, sodium, 0); } public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) { this.servingSize = servingSize; this.servings = servings; this.calories = calories; this.fat = fat; this.sodium = sodium; this.carbohydrate = carbohydrate; }}

當你使用這個類創建對象實例的時候,需要選擇相對應的構造方法:

1. NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);

一般情況下,這個構造方法的調用會需要許多不必要的參數,但是你必須要給它一些值。例如,在上述的例子中,我們給fat傳遞了一個0。如果只有6個參數,這也不是一個多麼難以接受的事情,但是當參數數量增長的時候,這種方式就有點難以忍受了。

簡單來說,重疊構造函數模式很有效,但是當參數很多時候寫起來很麻煩,閱讀也不友好。用戶必須仔細閱讀這些方法,並小心的計算參數的數量以避免出錯。很長的相同類型的參數容易導致一些微小的錯誤。當用戶把兩個參數搞反了,程序也不會報錯,但實際已經是錯誤的了。

第二個選擇是使用JavaBean的模式來解決這個問題,你可以調用一個無參數的構造函數來創建對象,然後使用set方法將所需的字段賦值,例如:

// JavaBeans Pattern - allows inconsistency, mandates mutabilitypublic class NutritionFacts { // Parameters initialized to default values (if any) private int servingSize = -1; // Required; no default value private int servings = -1; // Required; no default value private int calories = 0; private int fat = 0; private int sodium = 0; private int carbohydrate = 0; public NutritionFacts() { } // Setters public void setServingSize(int val) { servingSize = val; } public void setServings(int val) { servings = val; } public void setCalories(int val) { calories = val; } public void setFat(int val) { fat = val; } public void setSodium(int val) { sodium = val; } public void setCarbohydrate(int val) { carbohydrate = val; }}

這種模式沒有重疊構造函數模式的缺點,而且很容易構造,對代碼閱讀也很友好:

NutritionFacts cocaCola = new NutritionFacts();cocaCola.setServingSize(240);cocaCola.setServings(8);cocaCola.setCalories(100);cocaCola.setSodium(35);cocaCola.setCarbohydrate(27);

然而,JaveBeans本身有很大的缺點。由於構造過程有多次不同的調用,因此JavaBeans可能會產生不一致的情況。例如,JavaBeans類不能只通過檢查構造函數參數的有效性來保證一致性。當一個對象處於一種不一致的狀態時,試圖使用它可能會引起失敗,這個失敗很難從包含錯誤的代碼中去掉,因此很難調試。與此相關的一個缺點是JavaBeans的模式無法創建不可變的類,因此需要編程者花費其他成本來保證線程安全。

當構造工作完成時,可以通過手動『冰凍』對象並且在冰凍完成之前不允許使用它來彌補這個缺點,但這種方式太笨重了,在實踐中很少使用。而且,由於編譯器不能保證程序員在使用對象之前調用了冰凍方法,因此它可能在運行時引起錯誤。

幸運的是,有第三種方法既保證有重疊構造函數模式的安全性,也有JavaBeans的簡潔性。這就是生成器模式(Builder pattern)。客戶端使用構造方法來初始化所有必要的字段,然後使用類似setter方法來構建可選參數。最終,客戶端使用一個無參的builder方法來產生一個對象,通常該對象都是不可變的。Builder通常都是一個靜態的成員類:

// Builder Patternpublic class NutritionFacts { private final int servingSize; private final int servings; private final int calories; private final int fat; private final int sodium; private final int carbohydrate; public static class Builder { // Required parameters private final int servingSize; private final int servings; // Optional parameters - initialized to default values private int calories = 0; private int fat = 0; private int sodium = 0; private int carbohydrate = 0; public Builder(int servingSize, int servings) { this.servingSize = servingSize; this.servings = servings; } public Builder calories(int val) { calories = val; return this; } public Builder fat(int val) { fat = val; return this; } public Builder sodium(int val) { sodium = val; return this; } public Builder carbohydrate(int val) { carbohydrate = val; return this; } public NutritionFacts build() { return new NutritionFacts(this); } } private NutritionFacts(Builder builder) { servingSize = builder.servingSize; servings = builder.servings; calories = builder.calories; fat = builder.fat; sodium = builder.sodium; carbohydrate = builder.carbohydrate; }}

這個NutritionFacts就是不可變類,所有的默認參數都在一個地方。builder的setter方法返回builder本身從而使得可以鏈式調用API。調用的方法如下:

1. NutritionFacts cocaCola = new NutritionFacts.Builder(240,8).calories(100).sodium(35).carbohydrate(27).build();

這種API很容易寫,且閱讀起來也很方便。生成器模式模擬了Python和Scala中命名可選參數。

爲了簡短起見,參數的有效性檢驗在這裏沒有寫出來。爲了儘快的檢測到無效的參數,可以在builder的構造器和方法中檢驗。檢查有build方法調用的構造方法涉及到多個參數的不可變量。爲了防止這些不可變量收到攻擊,從builder中複製參數後對對象字段進行檢驗。如果檢測失敗,拋出IllegalArgumentException異常,可以顯示哪些參數是無效的。

生成器模式非常適合具有層次結構的類。使用並行的層次構造器,每一個都被嵌套在相關的類中。抽象類有抽象的builder; 具體的類有具體的builder。例如,考慮一個層次類的根節點是一個抽象類,代表了不同的pizza:

// Builder pattern for class hierarchiespublic abstract class Pizza { public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE } final Set toppings; abstract static class Builder> { EnumSet toppings = EnumSet.noneOf(Topping.class); public T addTopping(Topping topping) { toppings.add(Objects.requireNonNull(topping)); return self(); } abstract Pizza build(); // Subclasses must override this method to return "this" protected abstract T self(); } Pizza(Builder> builder) { toppings = builder.toppings.clone(); // See Item 50 }}

注意,Pizza.Builder是一個有着遞歸參數的通用類型(泛型)。它和抽象的self方法一起,允許子類中的方法進行鏈式調用,而不需要轉換。這個方法實際上是Java確實self類型的一個變通解決方案,這個類型通常稱爲模擬自我類型( the simulated self-type idiom)。

現在有兩個具體的Pizza子類,一個代表了標準的紐約式pizza,一個是意式包餡比薩(calzone)。前者需要大小(size)這個參數,後者需要指定醬要放在裏面還是外面。

public class NyPizza extends Pizza { public enum Size { SMALL, MEDIUM, LARGE } private final Size size; public static class Builder extends Pizza.Builder { private final Size size; public Builder(Size size) { this.size = Objects.requireNonNull(size); } @Override public NyPizza build() { return new NyPizza(this); } @Override protected Builder self() { return this; } } private NyPizza(Builder builder) { super(builder); size = builder.size; }}public class Calzone extends Pizza { private final boolean sauceInside; public static class Builder extends Pizza.Builder { private boolean sauceInside = false; // Default public Builder sauceInside() { sauceInside = true; return this; } @Override public Calzone build() { return new Calzone(this); } @Override protected Builder self() { return this; } } private Calzone(Builder builder) { super(builder); sauceInside = builder.sauceInside; }}

注意到,子類的builder被聲明爲返回正確的類型了。NyPizza.Builder的build方法返回的是NyPizza類,而Calzone.Builder返回的是Calzone類。這種子類方法返回父類返回值的子類型稱之爲協變返回類型(covariant return typing)。它允許子類可以直接使用這些builders而不需要做強制轉化。

查看原文 >>
相關文章