靜态工廠和構造器有個共同的局限性:它們都不能很好地擴充到大量的可選參數。比如用一個類表示包裝食品外面顯示的營養成分标簽。這些标簽中有幾個域是必需的:每份的含量、每罐的含量以及每份的卡路裡。還有超過20個的可選域:總脂肪量、飽和脂肪量、轉化脂肪、膽固醇、鈉等等。大多數産品在某幾個可選域中都會有非零的值。
對于這樣的類,應該用哪種構造器或者靜态工廠來編寫呢?程式員一向習慣采用重疊構造器模式,在這種模式下,提供的第一個構造器隻有必要的參數,第二個構造器有一個可選參數,第三個構造器有兩個可選參數,以此類推,最後一個構造器包含所有可選的參數。下面有個示例,為了簡單起見,它隻顯示四個可選域:
final修飾的變量表示指派之後不能再進行更改,系統賦預設值也算指派,是以系統也不會賦預設值
/**
* 營養成分
*/
public class NutritionFacts {
private final int servingSize; // 每份含量 required
private final int servings; // 每罐含量 required
private final int calories;// 卡路裡/罐 optional
private final int fat;// 脂肪/罐 optional
private final int sodium; // 鈉/罐 optional
private final int carbohydrate; // 碳水/罐 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個參數,看起來還不算太糟糕,問題是随着參數數目的增加,它很快就失去了控制。
簡而言之,重疊構造器模式可行,但是當有很多參數的時候,用戶端代碼會很難編寫,并且仍然很難閱讀。如果讀者想知道那些值是什麼意思,必須很仔細地數着這些參數來探個究竟。一長串類型相同的參數會導緻一些微妙的錯誤。如果用戶端不小心颠倒了其中兩個參數的順序,編譯器也不會出錯,但是程式在運作時會出現錯誤的行為
遇到許多可選的構造器參數的時候,還有第二種代替辦法,即JavaBeans模式,在這種模式下,取消掉final修飾符,先調用一個無參構造器來建立對象,然後再調用setter 方法來設定每個必要的參數,以及每個相關的可選參數
/**
* 營養成分
*/
public class NutritionFacts {
private int servingSize; // 每份含量 required
private int servings; // 每罐含量 required
private int calories;// 卡路裡/罐 optional
private int fat;// 脂肪/罐 optional
private int sodium; // 鈉/罐 optional
private int carbohydrate; // 碳水/罐 optional
public int getServingSize() {
return servingSize;
}
public void setServingSize(int servingSize) {
this.servingSize = servingSize;
}
public int getServings() {
return servings;
}
public void setServings(int servings) {
this.servings = servings;
}
public int getCalories() {
return calories;
}
public void setCalories(int calories) {
this.calories = calories;
}
public int getFat() {
return fat;
}
public void setFat(int fat) {
this.fat = fat;
}
public int getSodium() {
return sodium;
}
public void setSodium(int sodium) {
this.sodium = sodium;
}
public int getCarbohydrate() {
return carbohydrate;
}
public void setCarbohydrate(int carbohydrate) {
this.carbohydrate = carbohydrate;
}
}
這種模式彌補了重疊構造器模式的不足。說得明白一點,就是建立執行個體很容易,這樣産生的代碼讀起來也很容易:
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
遺憾的是,JavaBeans 模式自身有着很嚴重的缺點。因為構造過程被分到了幾個調用中, 在構造過程中,JavaBeans 可能處于不一緻的狀态。類無法僅僅通過檢驗構造器參數的有效性來保證一緻性。試圖使用處于不一緻狀态的對象将會導緻失敗,這種失敗與包含錯誤的代碼大相徑庭,是以調試起來十分困難。與此相關的另一點不足在于,JavaBeans 模式使得把類做成不可變的可能性不複存在,這就需要程式員付出額外的努力來確定它的線程安全。
幸運的是,還有第三種替代方法,它既能保證像重疊構造器模式那樣的安全性,也能保證像JavaBeans 那麼好的可讀性。這就是建造者(Builder)模式的一種形式,它不直接生成想要的對象,而是讓用戶端利用所有必要的參數調用構造器(或者靜态工廠),得到一個builder 對象。然後用戶端在builder 對象上調用類似于setter 的方法,來設定每個相關的可選參數。最後,用戶端調用無參的build 方法來生成通常是不可變的對象。這個builder 通常是它建構的類的靜态成員類,下面就是它的示例:
/**
* 營養成分
*/
public class NutritionFacts {
private final int servingSize; // 每份含量 required
private final int servings; // 每罐含量 required
private final int calories;// 卡路裡/罐 optional
private final int fat;// 脂肪/罐 optional
private final int sodium; // 鈉/罐 optional
private final int carbohydrate; // 碳水/罐 optional
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{
private int servingSize; // (ml) 每份含量 required
private int servings; // (per container) 每罐含量 required
private int calories;// (per serving) 卡路裡/每罐 optional
private int fat;// (g/serving)脂肪 g/罐 optional
private int sodium; // (mg/serving) 鈉 mg/罐 optional
private int carbohydrate; //(g/serving) 碳水 g/罐 optional
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);
}
}
}
注意 NutritionFacts 是不可變的,所有的預設參數值都單獨放在一個地方。builder的設定方法傳回自身,以便把調用連結起來,得到一個流式API。下面就是其用戶端代碼:
NutritionFacts cocaCola = new NutritionFacts
.Builder(240,8)
.calories(100)
.sodium(35)
.carbohydrate(27)
.build();
這樣的用戶端代碼很容易編寫,更為重要的是易于閱讀。Builder 模式模拟了可選參數
為了簡潔起見,示例中省略了有效性檢查。要想盡快偵測到無效的參數,可以在builder 的構造器和方法中檢查參數的有效性。檢視不可變量,包括build方法調用的構造器中的多個參數。為了確定這些不變量免受攻擊,從builder 複制完參數之後,要檢查對象域(詳見第50條)。如果檢查失敗就抛出 IllegalArgumentException,其中的詳細資訊會說明哪些參數是無效的。
與構造器相比,builder 的微弱優勢在于,它可以有多個可變(varargs)參數。因為builder 是利用單獨的方法來設定每一個參數。
Builder 模式的确也有它自身的不足。為了建立對象,必須先建立它的建構器。雖然建立這個建構器的開銷在實踐中可能不那麼明顯,但是在某些十分注重性能的情況下,可能就成問題了。Builder 模式還比重疊構造器模式更加冗長,是以它隻在有很多參數的時候才使用,比如4個或更多。但是記住,将來你可能需要添加參數。如果一開始就使用構造器或靜态工廠,等到類需要多個參數時才添加構造器,就會無法控制,那些過時的構造器或者靜态工廠顯得十分不協調。是以,通常最好一開始就使用建構器(Builder)。
簡而言之,如果類的構造器或者靜态工廠中具有多個參數,設計這種類時,Builder模式就是一種不錯的選擇,特别是當大多數參數都是可選或者類型相同的時候。與使用重疊構造器模式相比,使用Builder 模式的用戶端将更易于閱讀和編寫,建構器也比JavaBeans 更加安全
如果是内部調用較多,确定沒有安全問題,JavaBean模式和builder 已經非常接近了,并且,可以使用鍊式調用的方式,讓JavaBean 的調用顯得更簡潔,更像builder
/**
* 營養成分
*/
public class NutritionFacts {
private int servingSize; // 每份含量 required
private int servings; // 每罐含量 required
private int calories;// 卡路裡/罐 optional
private int fat;// 脂肪/罐 optional
private int sodium; // 鈉/罐 optional
private int carbohydrate; // 碳水/罐 optional
public NutritionFacts setServingSize(int servingSize) {
this.servingSize = servingSize;
return this;
}
public NutritionFacts setServings(int servings) {
this.servings = servings;
return this;
}
public NutritionFacts setCalories(int calories) {
this.calories = calories;
return this;
}
public NutritionFacts setFat(int fat) {
this.fat = fat;
return this;
}
public NutritionFacts setSodium(int sodium) {
this.sodium = sodium;
return this;
}
public NutritionFacts setCarbohydrate(int carbohydrate) {
this.carbohydrate = carbohydrate;
return this;
}
public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts()
.setServingSize(240)
.setServings(8)
.setCalories(100)
.setSodium(35)
.setCarbohydrate(27);
}
}
當然,現在很多公司和團隊已經開始積極使用Lombok 來簡化bean 對象。可以如下
/**
* 營養成分
*/
@Getter
@Setter
@Accessors(chain = true)
public class NutritionFacts {
private int servingSize; // 每份含量 required
private int servings; // 每罐含量 required
private int calories;// 卡路裡/罐 optional
private int fat;// 脂肪/罐 optional
private int sodium; // 鈉/罐 optional
private int carbohydrate; // 碳水/罐 optional
public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts()
.setServingSize(240)
.setServings(8)
.setCalories(100)
.setSodium(35)
.setCarbohydrate(27);
}
}