靜態(tài)工廠和構(gòu)造子都有一個限制:它們不能很好調(diào)節(jié)大量可選參數(shù)≈褡幔考慮這個情況敬飒,一個類表示包裝食品上的營養(yǎng)成分標(biāo)簽。這些標(biāo)簽有些是必需的域鬼佣,食用份量驶拱、份數(shù)和每份卡路里數(shù);還有二十多個可選域晶衷,總脂肪含量、飽和脂肪含量阴孟、反脂肪含量晌纫、膽固醇含量,鈉含量等等永丝。大多數(shù)產(chǎn)品中這些可選參數(shù)只有很少的非零值锹漱。
對于這樣的類,你打算用什么類型的構(gòu)造子或者靜態(tài)工廠慕嚷?習(xí)慣上哥牍,編程人員用重疊構(gòu)造子(telescoping constructor)模式:提供僅僅必需參數(shù)的構(gòu)造子,有單個可選參數(shù)的另外一個構(gòu)造子喝检,和有兩個可選參數(shù)的第三個構(gòu)造子嗅辣,等等,最后有所有可選參數(shù)的構(gòu)造子挠说。這是實(shí)踐中的樣子澡谭。為了簡單起見,只展示四個可選參數(shù):
// 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; // (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;
}
}
當(dāng)你想創(chuàng)建一個實(shí)例损俭,用包含你想設(shè)置所有參數(shù)的最短參數(shù)列表的構(gòu)造子:
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
通常蛙奖,調(diào)用這樣的構(gòu)造子需要你不想設(shè)置的參數(shù),但是你被迫為它們傳遞參數(shù)杆兵。這種情況下雁仲,你傳遞0這個值給fat。對于“只有”六個參數(shù)的情況琐脏,這個看上去不算很糟攒砖,但是當(dāng)參數(shù)數(shù)量增加時,很快就會失控。
簡而言之祭衩,重疊構(gòu)造子模式奏效灶体,但是當(dāng)有很多參數(shù)的時候,寫客戶端代碼是困難的掐暮,讀代碼更困難蝎抽。閱讀的人會想,這些值是什么路克,而且必須細(xì)心計(jì)算參數(shù)來弄明白樟结。相同類型參數(shù)的長長的系列會造成微妙的bug。如果客戶端不慎顛倒了兩個這樣的參數(shù)精算,編譯器不會發(fā)現(xiàn)瓢宦,但是程序在運(yùn)行時異常(條目51)。
當(dāng)面臨許多可選參數(shù)的構(gòu)造子時灰羽,第二個替代方案是JavaBean模式驮履,這個模式中,你調(diào)用一個無參數(shù)的構(gòu)造子來創(chuàng)建一個對象廉嚼,然后調(diào)用設(shè)置方法來設(shè)置每個必需的參數(shù)和感興趣的可選參數(shù):
// 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; // 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; }
}
這個模式?jīng)]有重疊構(gòu)造子模式的缺點(diǎn)玫镐。雖然有點(diǎn)啰嗦,但是容易創(chuàng)建實(shí)例怠噪,代碼也容易閱讀:
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
不幸的是恐似,JavaBean模式有它自己的嚴(yán)重缺點(diǎn)。因?yàn)闃?gòu)造被分成多個調(diào)用傍念,一個JavaBean在構(gòu)造的過程中有可能不一致的狀態(tài)矫夷。這個類沒有這個選擇:檢查構(gòu)造子參數(shù)的有效性來實(shí)行一致性。嘗試使用一個不一致狀態(tài)的對象憋槐,會導(dǎo)致失敗双藕,這個失敗與含有bug的代碼大相徑庭,所以難于調(diào)試秦陋。一個相應(yīng)的缺點(diǎn)是蔓彩,JavaBean模式限制了生成一個不變類的可能性,需要編碼者花額外的功夫保證線性安全驳概。
手動“凍結(jié)(freezing)”對象來減少這些缺點(diǎn)是可能的:當(dāng)對象構(gòu)造完成了赤嚼,不允許使用直到凍結(jié)。但是這個變體是笨拙的顺又,在實(shí)踐中用的非常稀少更卒。此外,在運(yùn)行中會造成錯誤稚照,因?yàn)樵谑褂盟磅蹇眨幾g器不能保證編碼者調(diào)用一個對象的凍結(jié)方法俯萌。
幸運(yùn)的是,有第三種選擇上枕,可以結(jié)合重疊構(gòu)造子模式的安全性和JavaBean模式的可讀性咐熙。這個是Builder模式[Gamma95]的一種形式。不是直接生成對象辨萍,而是客戶端調(diào)用有所有必需參數(shù)的一個構(gòu)造子(或者靜態(tài)工廠)棋恼,獲得一個builder對象。然后客戶端在builder對象上調(diào)用類似設(shè)置方法锈玉,分別設(shè)置感興趣的可選參數(shù)爪飘。最后,客戶端調(diào)用無參數(shù)的build方法來生成對象拉背,這個對象一般是不可變的师崎。builder是一個它構(gòu)建的類的靜態(tài)成員類(條目24)。下面是在實(shí)踐中的樣子:
// 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;
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類是不變的椅棺,所有參數(shù)的默認(rèn)值在一個地方犁罩。builder的設(shè)置方法返回builder自身,可以鏈?zhǔn)秸{(diào)用两疚,所以有流暢的API昼汗。下面是客戶端代碼的樣子:
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();
客戶端代碼容易寫,更重要的是也容易通讀鬼雀。builder模式模仿了在Python和Scala中有名字的可選參數(shù)。
為了簡單蛙吏,省略了有效性檢查源哩。為了盡快發(fā)現(xiàn)無效的參數(shù),在builder構(gòu)造子和方法中檢查參數(shù)的有效性鸦做。build方法調(diào)用的有多個參數(shù)的構(gòu)造子励烦,檢查這些不變量。為了保證這些不變量不受攻擊泼诱,在拷貝builder的參數(shù)后(條目50)坛掠,務(wù)必對對象的域進(jìn)行檢查。如果檢查失敗治筒,拋出IllegalArgumentException屉栓,它的具體信息表示哪些參數(shù)是無效的(條目75)。
builder模式非常適合類繼承耸袜。使用并行的builder的層級友多,每個builder嵌套在對應(yīng)的類中。抽象的類有抽象的builder堤框;具體的類有具體的builder域滥。比如纵柿,考慮一個抽象類是代表各種種類披薩的層級的根類:
// Builder pattern for class hierarchies
public abstract class Pizza {
public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
final Set<Topping> toppings;
abstract static class Builder<T extends Builder<T>> {
EnumSet<Topping> 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是一個有遞歸類型參數(shù)(recursive type parameter)(條目30)的泛型(generic type)启绰。這個和抽象的self方法一起昂儒,可以在子類中使得鏈?zhǔn)椒椒ㄗ嘈В恍枰獜?qiáng)行轉(zhuǎn)換委可。Java缺少self類型渊跋,這個變通方案被認(rèn)為是模擬self類型的慣例。
Pizza有兩個具體的子類撤缴,一個是紐約類型的比薩刹枉,另外一個是半月比薩。前者有指定大小的參數(shù)屈呕,而后者需要指定醬汁是在外面還是里面微宝。
public class NyPizza extends Pizza {
public enum Size { SMALL, MEDIUM, LARGE }
private final Size size;
public static class Builder extends Pizza.Builder<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<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中的build方法是聲明返回正確的子類的:NyPizza.Builderde的build方法返回NyPizza虎眨,而Calzone.Builder的build方法返回Calzone蟋软。這個技巧,一個子類方法被聲明返回一個聲明在超類中返回類型的子類型嗽桩,叫做協(xié)變返回類型(covariant return typing)岳守。這些層級builder的客戶端代碼,本質(zhì)上與簡單的NutritionFacts builder代碼是等同的碌冶。
客戶端的代碼例子如下湿痢,為了簡潔,假設(shè)enum常量是靜態(tài)導(dǎo)入:
NyPizza pizza = new NyPizza.Builder(SMALL)
.addTopping(SAUSAGE).addTopping(ONION).build();
Calzone calzone = new Calzone.Builder()
.addTopping(HAM).sauceInside().build();
相對于構(gòu)造子扑庞,builder有個小優(yōu)點(diǎn)是譬重,builder可以有多個可變參數(shù),因?yàn)槊總€參數(shù)是它自己方法中指定的罐氨⊥喂妫或者,build可以把傳入到多次調(diào)用的方法的參數(shù)聚合到單個域栅隐,就像前面addTopping方法展示的一樣塔嬉。
builder模式相當(dāng)靈活。單個builder可以重復(fù)使用到構(gòu)建多個對象租悄。隨著創(chuàng)建對象的不同谨究,builder的參數(shù)在build方法的調(diào)用之間可以改變。對象創(chuàng)建時builder可以自動填充到域中恰矩,比如隨著每次創(chuàng)建時一個增加的系列數(shù)记盒。
builder模式也有缺點(diǎn),為了創(chuàng)建一個對象外傅,必須首先創(chuàng)建它的builder纪吮。創(chuàng)建builder的代價在實(shí)踐中雖然是不顯而易見俩檬,但是在性能要求苛刻的情形中是一個問題。而且碾盟,builder模式相對于重疊構(gòu)造子模式更加啰嗦棚辽,所以如果有足夠的參數(shù),比如四個或者更多冰肴,才值得使用屈藐。但是記住,未來你有可能添加更多的參數(shù)熙尉。但是联逻,如果你開始使用構(gòu)造子或者靜態(tài)工廠,然后當(dāng)類演變到參數(shù)個數(shù)失控的時候检痰,切換到builder包归,這時廢棄的構(gòu)造子或者靜態(tài)工廠一直存在,這是個尷尬的事情铅歼。
總之公壤,在設(shè)計(jì)類時,當(dāng)它的構(gòu)造子或者靜態(tài)工廠有多過一些參數(shù)的時候椎椰,builder模式是一個好的選擇厦幅,特別是當(dāng)多個參數(shù)是可選的或者是同個類型的時候。和重疊構(gòu)造子相比慨飘,客戶端代碼更容易閱讀和編寫确憨,而且builder比JavaBeans更安全怀泊。