Effective Java 第三版讀書筆記——條款2:當構造器引數太多時考慮使用 builder 模式
靜態工廠方法和構造器都有一個限制:不能很好地支援可選引數(optional parameters)很多的類。考慮一個代表包裝食品上營養成分標籤的類:這些標籤有幾個必需的屬性(每份建議攝入量、每個包裝所含的份數、每份的卡路里)和超過二十個可選的屬性(總脂肪、飽和脂肪、反式脂肪、鈉等等)。應該為這樣的類編寫什麼樣的構造方法或靜態工廠呢?有沒有其他的方法解決這個問題?
Telescoping constructor 模式
提供一個只有必需引數的構造器,第二個構造器有一個可選引數,第三個構造器有兩個可選引數……最後一個構造器有所有的可選引數。例如:
// 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;
}
}
建立物件時,選擇滿足條件的構造器中引數最少的那一個:
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
顯然,建立物件時你會為很多自己不感興趣的引數賦初始值,這使得客戶端程式碼的書寫變得困難。此外,這樣的程式碼也很難閱讀,讀程式碼的人需要了解引數的順序並且仔細數清楚引數的個數。總之,Telescoping constructor 模式不是一種很好的解決方案。
JabaBeans 模式
在這種模式中,呼叫一個無引數的建構函式來建立物件,然後呼叫setter方法來設定每個必需引數和可選引數。例如:
// JavaBeans Pattern - allows inconsistency, mandates mutability (pages 11-12)
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; }
}
這個模式沒有 telescoping constructor 模式的缺點,建立物件十分容易並且可讀性也很高:
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
不幸的是,JavaBeans 模式有很嚴重的缺點:
- 在構造過程中,一個 JavaBean 可能處於不一致狀態。如果在它還沒有構造完全時另一個執行緒就呼叫了它,就會帶來很多意想不到的 bug。
- JavaBeans 模式排除了讓類不可變的可能性。因為 setter 方法的存在,JavaBean 必須是可變的,但有時我們想要建立不可變的物件。
由此可知,JavaBeans 模式也不是一種很好的解決方案。
Builder 模式
結合了 telescoping constructor 模式的安全性與 JavaBeans 模式的易讀性。客戶端不直接建立所需類的物件,而是呼叫構造方法(或靜態工廠),並使用所有必需引數來獲得一個 builder 物件。然後,客戶端呼叫 builder 物件的類似 setter
的方法來設定每個可選引數。最後,客戶端呼叫一個無參的 build
方法來生成物件,該物件通常是不可變的。Builder 通常是它所構建的類的一個靜態成員類。例如:
// Builder Pattern (Page 13)
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
類是不可變的,所有的引數預設值都在一個地方。 builder 的 setter 方法返回 builder 本身,這樣呼叫就可以被連結起來,從而生成一個流暢的API。下面是客戶端程式碼的示例:
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();
Builder 模式非常靈活。單個 builder 可以重複使用來構建多個物件。 builder 可以在建立物件時自動填充一些屬性,例如每次建立物件時增加的序列號。
Builder 模式也有缺點。為了建立物件,首先必須建立它的 builder 。雖然建立這個 builder 的成本在實際應用中不太大,但在效能關鍵的情況下可能會出現問題。而且,builder 模式比 telescoping constructor 模式更冗長,因此只有在有足夠的引數時才值得使用它,比如四個或更多。
Builder 模式非常適合類層次結構
使用平行層次的 builder,每一個都巢狀在相應的類中。 抽象類有抽象的 builder ; 具體類有具體的 builder。 例如,考慮代表各種比薩餅的根層次結構的抽象類:
// Builder pattern for class hierarchies (Page 14)
// Note that the underlying "simulated self-type" idiom allows for arbitrary fluid hierarchies, not just builders
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
是一個帶有遞迴型別引數( recursive type parameter)(條款30)的泛型型別。 這與抽象的 self
方法結合在一起,允許方法鏈在子類中正常工作,而不需要強制轉換。
這裡有兩個具體的 Pizza
的子類,其中一個代表標準的紐約風格的披薩,另一個是半圓形烤乳酪披薩。前者有一個所需的尺寸引數,而後者則允許指定醬汁應該在裡面還是外面:
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;
}
}
下面是一個客戶端程式碼的樣例:
import static effectivejava.chapter2.item2.hierarchicalbuilder.Pizza.Topping.*;
import static effectivejava.chapter2.item2.hierarchicalbuilder.NyPizza.Size.*;
// Using the hierarchical builder (Page 16)
public class PizzaTest {
public static void main(String[] args) {
NyPizza pizza = new NyPizza.Builder(SMALL)
.addTopping(SAUSAGE).addTopping(ONION).build();
Calzone calzone = new Calzone.Builder()
.addTopping(HAM).sauceInside().build();
}
}