靜態(tài)??與構(gòu)造?法有?個共通的限制:當(dāng)存在?量的可選參數(shù)時,他們的可伸縮性很差。考慮這樣?個類,它表示貼在包裝好的?品上的營養(yǎng)表標(biāo)簽。這些標(biāo)簽有?些必要的字段,如分量??、每瓶容量以及每份的卡路?數(shù),還有20多個可選字段,如總脂肪量、飽和脂肪酸、反式脂肪酸、膽固醇及鈉元素等等。對于?多數(shù)產(chǎn)品來說,只有少量的這些可選字段有?零值。
對于這樣?個類來說,你應(yīng)該編寫哪種構(gòu)造?法或是靜態(tài)??呢?傳統(tǒng)上,程序員們會使?重疊構(gòu)造?法模式,在這種模式中,你會提供?個只接收必要參數(shù)的構(gòu)造?法,然后編寫?個接收單個可選參數(shù)的構(gòu)造?法,再編寫?個接收兩個可選參數(shù)的構(gòu)造?法,以此類推,最后提供?個接收所有可選參數(shù)的構(gòu)造?法。如下代碼示例就說明了這?點。出于簡潔的?的,這?只給出了4個可選字段:
// JavaBeans Pattern - allows inconsistency, mandates mutability
// Telescoping constructor pattern - does not scale well!
public class NutritionFacts {
private final int servingSize; // (mL) required
private int servings; // (per container) required
private int calories; // (per serving) optional
private int fat; // (g/serving) optional
private 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;
}
}
在創(chuàng)建實例時,你可以使?包含了所要設(shè)置的所有參數(shù)的最短參數(shù)列表構(gòu)造?法:
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
?般來說,這個構(gòu)造?法調(diào)?需要很多你并不想設(shè)置的參數(shù),但卻不得不為其傳值。在這種情況下,我們?yōu)?code>fat傳遞了0值。上述代碼『只有』6個參數(shù),看起來還不算太糟糕,不過隨著參數(shù)數(shù)量的增加,很快你就數(shù)不過來了。
??以蔽之,重疊構(gòu)造?法模式可以?,但當(dāng)參數(shù)數(shù)量過多時,客戶端代碼的編寫就變得愈發(fā)困難,閱讀起來也更費勁。閱讀者會想,所有這些值是什么意思,并且要??地檢查參數(shù)才能知道結(jié)果。??的同類型參數(shù)序列會導(dǎo)致?常隱秘的Bug。如果客戶端不??改變了這樣兩個參數(shù)的順序,那么編譯器是不知情的,不過程序在運?期則會表現(xiàn)出錯誤的?為(條款51)
當(dāng)構(gòu)造?法中存在?量可選參數(shù)時,另?種解決?案是JavaBeans模式。在這種模式下,你會通過?個?參構(gòu)造?法來創(chuàng)建對象,接下來調(diào)?setter?法設(shè)置每個必填參數(shù)與所需要的每個可選參數(shù):
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;
/*無參構(gòu)造方法*/
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)造?法模式的缺點。通過這種?式可以輕松創(chuàng)建實例(就是稍微有點冗?),并且代碼讀起來也?較容易:
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27)
但遺憾的是,JavaBeans模式?身存在嚴(yán)重的缺陷。由于構(gòu)造過程被劃分為多個調(diào)?,因此JavaBean在構(gòu)造過程中可能會處于?種不?致的狀態(tài)下。類僅僅通過檢查構(gòu)造?法參數(shù)的有效性是?法確保?致性的。當(dāng)對象處于不?致狀態(tài)時,使?這個對象會導(dǎo)致難以覺察的Bug,這種Bug也極難調(diào)試。與之相關(guān)的另?個缺陷就是JavaBeans模式?法確保?個類的不變性(條款17),并且需要程序員??確保線程安全性。
當(dāng)構(gòu)造完畢時,我們可以通過??『凍結(jié)』對象并且直到凍結(jié)后才允許使?對象來消除這些缺陷,不過這種做法很少使?。此外,這么做會導(dǎo)致運?期錯誤,因為編譯器?法確保程序員在使?對象前會調(diào)?對象的凍結(jié)?法。
幸好,還有?種?案融合了重疊構(gòu)造?法模式的安全性與JavaBeans模式的可讀性。它是?種構(gòu)建器模式[Gamma95]。相?于直接創(chuàng)建所需的對象,客戶端會調(diào)??個構(gòu)造?法(或是靜態(tài)??),該構(gòu)造?法帶有所需的參數(shù),并且得到?個構(gòu)建器對象。接下來,客戶端會對構(gòu)建器對象調(diào)?類似于setter的?法來設(shè)置每?個感興趣的可選參數(shù)。最后,客戶端會調(diào)??個?參的build?法來?成對象,該對象是個不變對象。構(gòu)建器通常是它所構(gòu)建的類的?個靜態(tài)成員類(條款24)。如下代碼展示了其使??式:
public class NutritionFacts {
private int servingSize;
private int servings;
private int calories;
private int fat;
private int sodium;
private int carbohydrate;
/*私有構(gòu)造方法*/
private NutritionFacts(Builder builder){
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
/*靜態(tài)成員類 ---> 構(gòu)建器 */
public static class Builder {
private int servingSize;
private int servings;
private int calories;
private int fat;
private int sodium;
private int carbohydrate;
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){
this.sodium = val;
return this;
}
public Builder carbohydrate(int val){
carbohydrate = val;
return this;
}
/*返回要構(gòu)建的真正對象*/
public NutritionFacts build(){
return new NutritionFacts(this);
}
}
}
NutritionFacts類是不可變的,所有的參數(shù)默認值都在?個地?。構(gòu)建器的setter?法返回的是構(gòu)建器?身,這樣調(diào)?就可以鏈接起來,形成?種流式API。如下展示了客戶端代碼的樣?:
public class TestClient {
public static void main(String[] args) {
NutritionFacts test = new NutritionFacts.Builder(1,2).
calories(5)
.carbohydrate(6)
.sodium(7)
.fat(8)
.build();
System.out.println(test);
}
}
上述客戶端代碼編寫起來很容易,更為重要的是,閱讀起來也?常輕松。構(gòu)建器模式模擬了Python與Scala中的具名可選參數(shù)。
出于簡潔的?的,這?省略了有效性檢查。為了能盡快檢測出?效參數(shù),請在構(gòu)建器的構(gòu)造?法與?法中檢查參數(shù)的有效性。不變性檢查涉及到由build?法所調(diào)?的構(gòu)造?法中的多個參數(shù)。為了確保這些不變參數(shù)真正能夠做到不變,請在從構(gòu)建器中復(fù)制完參數(shù)后就對對象字段進?檢查(條款50)。如果檢查失敗,那就要拋出IllegalArgumentException異常(條款72),異常的詳細信息標(biāo)識出了哪些參數(shù)是?效的(條款75)。
構(gòu)建器模式?常適合于類繼承。使?平?的構(gòu)建器層次體系,每個都嵌套在對應(yīng)的類中。抽象類有抽象構(gòu)建器;具體類有具體構(gòu)建器。?如說,考慮如下這個抽象類,它是層次體系的根,表示各種類型的披薩:
// 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(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ù)(條款30)。通過該參數(shù)以及抽象的self?法可以讓?法在?類中恰當(dāng)?shù)劓溄悠饋恚??需進?類型轉(zhuǎn)換。這種對于Java缺乏?我類型問題的解決?案叫做模擬的?我類型。
如下是兩個具體的Pizza?類,?個代表標(biāo)準(zhǔn)的紐約?格披薩,另?個代表半圓形烤乳酪披薩。前者有?個必填的size參數(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 = 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;
}
}
注意到每個?類構(gòu)建器中的build?法都被聲明為返回正確的?類:NyPizza.Builder的build?法返回NyPizza,Calzone.Builder的build?法則返回Calzone。這種?類?法聲明為返回?類?法所聲明的返回類型的?類型的技術(shù)叫做協(xié)變返回類型??蛻舳丝梢酝ㄟ^這項技術(shù)在不借助于類型轉(zhuǎn)換的情況下使?這些構(gòu)建器。
這些『層次化構(gòu)建器』的客戶端代碼本質(zhì)上與簡單的NutritionFacts構(gòu)建器客戶端代碼別??致。出于簡潔的?的,如下的示例客戶端代碼假設(shè)已經(jīng)靜態(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)造?法來說,構(gòu)建器的?個??的優(yōu)勢在于構(gòu)建器可以擁有多個可變參數(shù),這是因為每個參數(shù)都是在??的?法中指定的。此外,構(gòu)建器可以將傳遞給多個調(diào)?的參數(shù)聚合起來,并通過?個?法傳給單個字段,這?點在之前的addTopping?法中已經(jīng)介紹過了
構(gòu)建器模式是相當(dāng)靈活的。單個構(gòu)建器可以重復(fù)多次使?來構(gòu)建多個對象。構(gòu)建器的參數(shù)可以在build?法的調(diào)?之間進?調(diào)整以改變所創(chuàng)建的對象。構(gòu)建器可以在對象創(chuàng)建時?動填充?些字段,?如說每次創(chuàng)建?個對象時遞增的序列號等。
構(gòu)建器模式也有?身的缺點。為了創(chuàng)建對象,你必須要先創(chuàng)建其構(gòu)建器。雖然在實際情況中,創(chuàng)建構(gòu)建器的成本并不是很?,但在性能關(guān)鍵的情況下這就會導(dǎo)致問題了。此外,構(gòu)建器模式要?重疊的構(gòu)造?法模式更加冗?,這樣只有在參數(shù)數(shù)量?夠多的情況下使?構(gòu)建器模式才是值得的,?如說4個以上的參數(shù)。不過請記住,你可能會在未來增加更多的參數(shù)。但如果?開始使?的是構(gòu)造?法或是靜態(tài)??,當(dāng)參數(shù)數(shù)量變得很多時,想要切換到構(gòu)建器,那么顯?易?,會遺留很多廢棄的構(gòu)造?法或是靜態(tài)??。因此,更好的做法則是?開始就使?構(gòu)建器。