靜態工廠和構造器有個共同的侷限性:它們都不能很好地擴展到大量的可選參數。考慮用一個類表示包裝食品外面顯示的養分成份標籤。這些標籤中有幾個域是必需的:每份的含量、每罐的含量以及每份的卡路里,還有超過20個可選域:總脂肪量、飽和脂肪量、轉化脂肪、膽固醇、鈉等等。大多數產品都只有幾個可選域中會有非零的值。 java
對於這樣的類,應該用哪一種構造器或者靜態方法來編寫呢?程序員一貫習慣採用telescoping constructor模式,在這種模式下,你提供一個只有必要參數的構造器,第二個構造器有一個可選參數,第三個有兩個可選參數,依此類推,最後一個構造器包含全部可選參數。下面有個示例,爲了簡單起見,它只顯示四個可選域:
node
// Telescoping constructor pattern - does not scale well! public class NutritionFacts { private final int servingSize; // (mL) required private final int servings; // (per container) required private final int calories; // optional private final int fat; // (g) optional private final int sodium; // (mg) optional private final int carbohydrate; // (g) 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; } }當你想要建立實例的時候,就利用參數列表最短的構造器,但該列表中包含了要設置的全部參數:
NutritionFacts cocaCola =new NutritionFacts(240, 8, 100, 0, 35, 27);這個構造器調用一般須要許多你本不想設置的參數,但仍是不得不爲它們傳遞值。在這種狀況下,咱們給fat傳遞了一個值爲0。若是"僅僅"是這6個參數,看起來還不算太糟,問題是隨着參數數目的增長,它很快就失去了控制。
一句話:telescoping constructor模式可行,可是當有許多參數的時候,客戶端代碼會很難編寫,而且仍然較難以閱讀。若是讀者想知道那些值是什麼意思,必須很仔細地數着這些參數來探個究竟。一長串相同的類型參數會致使一些微妙的錯誤。若是客戶端不當心顛倒了這兩種參數,編譯器也不會出錯,可是程序在運行時會出現錯誤的行爲。
程序員
遇到許多構造器參數的時候,還有第二種代替辦法,即JavaBeans模式,在這種模式下,調用一個無參構造器來建立對象,而後調用setter方法來設置每一個必要的參數,以及每一個相關的可選參數:
安全
// JavaBeans Pattern - allows inconsistency, mandates mutability public class NutritionFacts { // Parameters initialized to default values (if any) private int servingSize = -1; // Required; no default value private int servings = -1; // " " " " 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; } }這種模式不具有telescoping constructor模式的任何缺點。說得明白一點,就是建立實例很容易,這樣產生的代碼讀起來也很容易:
NutritionFacts cocaCola = new NutritionFacts(); cocaCola.setServingSize(240); cocaCola.setServings(8); cocaCola.setCalories(100); cocaCola.setSodium(35); cocaCola.setCarbohydrate(27);
遺憾的是,JavaBeans模式自身有着很嚴重的缺點。由於構造過程被分到了幾個調用中,在構造過程當中JavaBean可能處於不一致的狀態。類沒法僅僅經過檢驗構造器參數的有效性來保證一致性。試圖使用處於不一致狀態的對象,將會致使失敗,這種失敗與包含錯誤的代碼截然不同,所以它調試起來十分困難。與此相關的另外一點不足在於,JavaBeans模式防止了把類作成不可變的可能(見第15條),這就須要程序員付出格外努力來確保它的線程安全。 性能
當對象的構造完成,而且不容許在解凍以前使用時,經過手工"凍結"對象,能夠彌補這些不足,可是這種方式十分笨拙,在實踐中不多使用。此外,它甚至會在運行時致使錯誤,由於編譯器沒法確保程序員會在使用以前先在對象上調用freeze方法。
ui
幸運的是,還有第三種替代方法,既能保證像telescoping constructor模式那樣的安全性,也能保證像JavaBeans模式那麼好的的可讀性。這就是Builder模式[Gamma95,p.97]的一種形式。不直接生成想要的對象,而是讓客戶端利用全部必要的參數調用構造器(或者靜態工廠),獲得一個builder對象。而後客戶端在builder對象上調用相似於setter的方法,來設置每一個相關的可選參數。最後,客戶端調用無參的build方法來生成不可變的對象。這個builder是它構建的類的靜態成員類(見第22條)。下面就是它的示例: this
// Builder Pattern public 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; private NutritionFacts(Builder builder) { servingSize = builder.servingSize; servings = builder.servings; calories = builder.calories; fat = builder.fat; sodium = builder.sodium; carbohydrate = builder.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 carbohydrate = 0; private int sodium = 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 carbohydrate(int val) { carbohydrate = val; return this; } public Builder sodium(int val) { sodium = val; return this; } public NutritionFacts build() { return new NutritionFacts(this); } } }注意NutritionFacts是不可變的,全部的默認參數值都單獨放在一個地方。builder的setter方法返回builder自己,以即可以把調用連接起來。下面就是客戶端代碼:
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8). calories(100).sodium(35).carbohydrate(27).build();這樣的客戶端代碼很容易編寫,更爲重要的是,易於閱讀。builder模式模擬了具名的可選參數,就像Ada和Python中的同樣。
builder像個構造器同樣,能夠在它的參數中強加約束條件。build方法能夠檢驗這些約束條件。將參數從builder拷貝到對象中以後,並在對象域而不是builder域(見第39條)中對它們進行檢驗,這個很重要。若是違反了任何約束條件,build方法就應該拋出IllegalStateException(見第60條)。異常的詳細方法應該顯示出違反了哪一個約束條件。
spa
對多個參數強加約束條件的另外一種方法是,用setter方法提供某個約束條件必須持有的全部參數。若是該約束條件沒有獲得知足,setter方法就會拋出IllegalArgumentException。這有個好處,就是一旦傳遞了無效的參數,當即就會發現約束條件失敗,而不是等着調用build方法。
線程
與構造器相比,builder的一點微略優點在於,builder能夠有多個varargs參數。構造器就像方法同樣,只能有一個varargs參數。由於builder利用單獨的方法來設置每一個參數,你想要多少個varargs參數,它們就能夠有多少個,直到每一個setter方法都有一個varargs參數。
設計
Builder模式十分靈活,能夠利用單個builder構建多個對象。builder的參數能夠在建立對象期間進行調整,也能夠隨着不一樣的對象而改變。builder能夠自動填充某些域,例如每次建立對象時自動增長序列號。
設置了參數的builder生成了一個很好的抽象工廠(Abstract Factory)[Gamma95,p.87]。換句話說,客戶端能夠將這樣一個builder傳給方法,使該方法可以爲客戶端建立一個或者多個對象。要使用這種用法,須要有個類型來表示builder。若是使用的是發行版本1.5或者更新的版本,一個單獨的泛型(見第26條)就能知足全部的builder,不管它們在構建哪一種類型的對象:
// A builder for objects of type T public interface Builder { public T build(); }注意,能夠聲明NutritionFacts.Builder類來實現Builder 。
帶有Builder實例的方法一般利用有限制的通配符類型(bounded wildcard type,見第28條)來約束構建器的類型參數。例如,下面就是構建每一個節點的方法,它利用一個客戶端提供的Builder實例構建樹:
Tree buildTree(Builder nodeBuilder) { ... }Java中傳統的抽象工廠實現是Class對象,用newInstance方法充當build方法的一部分。這種用法隱含着許多問題。newInstance方法老是企圖調用類的無參構造器,這個構造器甚至可能根本不存在。若是類沒有能夠訪問的無參構造器,你也不會收到編譯時錯誤。相反,客戶端代碼必須在運行時處理InstantiationException或者IllegalAccessException,這樣既不雅觀也不方便。newInstance方法還會傳播由無參構造器拋出的任何異常,即便newInstance缺少相應的throws子句。換句話說,Class.newInstance破壞了編譯時的異常檢查。上面講過的Builder接口彌補了這些不足。
Builder模式的確也有它自身的不足。爲了建立對象,必須先建立它的構建器。雖然建立構建器的開銷在實踐中可能不那麼明顯,可是在某些十分注重性能的狀況下,可能就是個問題了。Builder模式還比telescoping constructor模式更加冗長,所以它只在有足夠參數的時候才使用,好比4個或者更多個參數。可是記住,未來你可能須要添加參數。若是一開始就使用構造器或者靜態工廠,等到類須要多個參數時才添加構建器,就會沒法控制,那些過期的構造器或者靜態工廠顯得十分不協調。所以,一般最好一開始就使用構建器。
簡而言之,若是類的構造器或者靜態工廠中具備多個參數,設計這種類時,Builder模式就是種不錯的選擇,特別是當大多數參數都是可選的時候。與使用傳統的telescoping constructor模式相比,使用Builder模式的客戶端代碼將更易於閱讀和編寫,builders也比JavaBeans更加安全。