第2項:當面臨多個參數的構造器時考慮使用構建器

  靜態工廠和構造器有個共同的侷限性:他們都不能很好地擴展到大量的可選參數。考慮用一個類表示包裝食品外面顯示的養分成分標籤。這些標籤中有幾個域是必需的:每份的含量、每罐的含量以及每份的卡路里,還有超過20個可選域:總脂肪、飽和脂肪量、轉化脂肪、膽固醇、鈉等等。大多數產品在某幾個可選域中都會有非零的值。java

  對於這樣的類,應該採用哪一種構造器或者靜態方法來編寫呢?程序猿一貫習慣採用重疊構造器(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);這個構造器調用一般須要許多你本不想設置的參數,但仍是不得不爲它們傳遞值。在這個例子中,咱們給fat傳遞了一個值爲0。若是「僅僅」是這6個參數,看起來還不算太糟,問題是隨着參數數目的增長,它很快就失去了控制。ide

  總的來講,使用重疊構造器模式是可行的,可是當有不少參數的時候就很難編寫客戶端代碼,也很難去閱讀它們。若是讀者想要知道這些值表明什麼意思,就必須仔細地數着這些參數來探個究竟。一長串類型相同的參數會致使一些微妙的錯誤,若是客戶端不當心顛倒了期中兩個參數的順序,編譯器也不會報錯,可是程序在運行的時候就會出現錯誤的行爲。函數

  遇到許多構造器參數的時候,還有第二種代替方法,即JavaBean模式,在這種模式下,調用一個無參構造器來建立對象,而後調用setter方法來設置每一個必要的參數,以及每一個相關的可選參數:性能

// 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; }
}

  這種模式彌補了重疊構造器模式的不足。說得明白一點,就是建立實例很容易,這樣產生的代碼讀起來也很容易。ui

NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);

  遺憾的是,這種JavaBean模式自身有很嚴重的缺點。由於構造過程被分到了幾個調用中,在構造的過程當中JavaBean可能處於不一致的狀態。類沒法經過檢驗構造器參數的有效性來保證一致性。試圖使用處於不一致狀態的對象,將會致使失敗,這種失敗與包含錯誤的代碼截然不同,所以它調試起來十分困難。與此相關的另外一點不足在於,JavaBean模式阻止了把類作成了不可變的可能(第17項),這就須要程序猿付出額外的努力來保證它的線程安全。this

  在構造器完成構造對象以前進行加鎖,完成構造以後進行解鎖,這就能彌補以上的不足之處,可是這種方式十分笨拙,在實踐中不多使用。此外,它甚至會在運行時出現錯誤,由於編譯器沒法確保程序猿會在使用構造器以前進行加鎖操做。spa

  幸運的是,還有第三種替代方法,結合了重疊構造器的安全性和JavaBean模式的可讀性。這就是Builder模式 [Gamma95] 的一種形式。不直接生成想要的對象,而是讓客戶端調用一個帶有全部必需參數的構造器方法(或者靜態工廠方法)去得到一個builder對象,而後客戶端在builder對象上調用相似於setter的方法來設置每一個相關的可選參數。最後,客戶端調用無參的build方法來生成不可變的對象。這個builder一般是它構建的類的靜態成員類(第24項)。下面就是它的示例:線程

// 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是不可變的,全部默認參數值都單獨放在一個地方。buildersetter方法返回的是builder自己,以即可以把調用鏈接起來。下面是客戶端代碼:設計

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).carbohydrate(27).build();

  這樣的客戶端代碼是很容易編寫的,更重要的是,閱讀起來很容易。Builder模式模仿了Python和Scala中的命名可選參數。

  爲簡潔起見,省略了有效性檢查。 要儘快檢測無效參數,請在構建器的構造函數和方法中檢查參數有效性。 檢查構建方法調用的構造函數中涉及多個參數的不變量。 要確保這些不變量不受攻擊,請在從構建器複製參數後對對象字段執行檢查(第50項)。 若是檢查失敗,則拋出IllegalArgumentException(第72項),其詳細消息指示哪些參數無效(第75項)。

  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是具備遞歸類型參數的通用類型(第30項)。 這與抽象方法self一塊兒容許方法鏈在子類中正常工做,而不須要強制轉換。Java缺少自我類型這一事實的解決方法被稱爲模擬自我類型習語(This workaround for the fact that Java lacks a self type is known as the simulated self-type idiom.)。

  這是Pizza的兩個具體子類,其中一個表明標準的紐約式披薩,另外一個表明calzone。 前者具備所需的大小參數,然後者容許你指定醬汁應該在內部仍是外部:

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

  請注意,每一個子類的構建器中的構建方法被聲明爲返回正確的子類:NyPizza.Builder的構建方法返回NyPizza,而Calzone.Builder中的構建方法返回Calzone。這種技術,其中子類方法聲明的返回類型是在超類中聲明的返回類型的子類型,稱爲協變返回類型,它容許客戶使用這些構建器而無需進行建立。

  這些「分層構建器」的客戶端代碼基本上與簡單的NutritionFacts構建器的代碼相同。爲簡潔起見,下面顯示的示例客戶端代碼假定枚舉常量上的靜態導入:

NyPizza pizza = new NyPizza.Builder(SMALL).addTopping(SAUSAGE).addTopping(ONION).build();
Calzone calzone = new Calzone.Builder().addTopping(HAM).sauceInside().build();

  構建器相對於構造函數的一個小優勢是構建器能夠有多個可變參數,由於每一個參數都是在本身的方法中指定的。除此以外,構建器能夠將傳遞給一個方法的多個參數經過屢次調用方法的方式聚合到一個字段中,就如以前addTopping方法中所演示的那樣。

  Builder模式很是靈活。 能夠重複使用單個構建器來構建多個對象。 能夠在構建方法的調用之間調整構建器的參數,以改變建立的對象。構建器能夠在建立對象時自動填充某些字段,例如每次建立對象時增長的序列號。

  Builder模式也有缺點。 要建立對象,必須先建立其構建器。 雖然在實踐中建立此構建器的成本不太可能明顯,但在性能關鍵的狀況下可能會出現問題。此外,Builder模式比重疊構造函數的模式更冗長,所以只有在有足夠的參數(例如四個或更多)時才值得去使用它。 但請記住,你可能但願在未來添加更多的參數。可是若是你從構造函數或靜態工廠開始並在類進化到參數數量失控時才切換到構建器,那些過期的構造器和靜態工廠就會顯得很是不協調。所以,最好一開始就考慮使用構造器。

  簡而言之,若是類的構造器或者靜態工廠方法中具備多個參數,設計這種類時,Builder模式就是種不錯的選擇,特別是當大多數參數都是可選的時候。與使用傳統的重疊構造器模式相比,使用Builder模式的客戶端代碼更易於閱讀和編寫,構建器也比JavaBean更加安全。

我的公衆號同步更新

相關文章
相關標籤/搜索