1. 程式人生 > >Effective Java 第三版讀書筆記——條款2:當構造器引數太多時考慮使用 builder 模式

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();
        
    }
}