天天看點

建立和銷毀對象——遇到多個構造器參數時要考慮使用建構器

靜态工廠和構造器有個共同的局限性:它們都不能很好地擴充到大量的可選參數。比如用一個類表示包裝食品外面顯示的營養成分标簽。這些标簽中有幾個域是必需的:每份的含量、每罐的含量以及每份的卡路裡。還有超過20個的可選域:總脂肪量、飽和脂肪量、轉化脂肪、膽固醇、鈉等等。大多數産品在某幾個可選域中都會有非零的值。 

對于這樣的類,應該用哪種構造器或者靜态工廠來編寫呢?程式員一向習慣采用重疊構造器模式,在這種模式下,提供的第一個構造器隻有必要的參數,第二個構造器有一個可選參數,第三個構造器有兩個可選參數,以此類推,最後一個構造器包含所有可選的參數。下面有個示例,為了簡單起見,它隻顯示四個可選域:

final修飾的變量表示指派之後不能再進行更改,系統賦預設值也算指派,是以系統也不會賦預設值

/**
 * 營養成分
 */
public class NutritionFacts {

    private final int servingSize; //  每份含量             required
    private final int servings; //    每罐含量     required

    private final int calories;// 卡路裡/罐      optional
    private final int fat;//   脂肪/罐              optional
    private final int sodium; //   鈉/罐         optional
    private final int carbohydrate; // 碳水/罐     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個參數,看起來還不算太糟糕,問題是随着參數數目的增加,它很快就失去了控制。

簡而言之,重疊構造器模式可行,但是當有很多參數的時候,用戶端代碼會很難編寫,并且仍然很難閱讀。如果讀者想知道那些值是什麼意思,必須很仔細地數着這些參數來探個究竟。一長串類型相同的參數會導緻一些微妙的錯誤。如果用戶端不小心颠倒了其中兩個參數的順序,編譯器也不會出錯,但是程式在運作時會出現錯誤的行為

遇到許多可選的構造器參數的時候,還有第二種代替辦法,即JavaBeans模式,在這種模式下,取消掉final修飾符,先調用一個無參構造器來建立對象,然後再調用setter 方法來設定每個必要的參數,以及每個相關的可選參數

/**
 * 營養成分
 */
public class NutritionFacts {

    private  int servingSize; //  每份含量             required
    private  int servings; //    每罐含量     required

    private  int calories;// 卡路裡/罐      optional
    private  int fat;//   脂肪/罐              optional
    private  int sodium; //   鈉/罐         optional
    private  int carbohydrate; // 碳水/罐     optional

    public int getServingSize() {
        return servingSize;
    }

    public void setServingSize(int servingSize) {
        this.servingSize = servingSize;
    }

    public int getServings() {
        return servings;
    }

    public void setServings(int servings) {
        this.servings = servings;
    }

    public int getCalories() {
        return calories;
    }

    public void setCalories(int calories) {
        this.calories = calories;
    }

    public int getFat() {
        return fat;
    }

    public void setFat(int fat) {
        this.fat = fat;
    }

    public int getSodium() {
        return sodium;
    }

    public void setSodium(int sodium) {
        this.sodium = sodium;
    }

    public int getCarbohydrate() {
        return carbohydrate;
    }

    public void setCarbohydrate(int carbohydrate) {
        this.carbohydrate = carbohydrate;
    }
}
           

這種模式彌補了重疊構造器模式的不足。說得明白一點,就是建立執行個體很容易,這樣産生的代碼讀起來也很容易:

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

遺憾的是,JavaBeans 模式自身有着很嚴重的缺點。因為構造過程被分到了幾個調用中, 在構造過程中,JavaBeans 可能處于不一緻的狀态。類無法僅僅通過檢驗構造器參數的有效性來保證一緻性。試圖使用處于不一緻狀态的對象将會導緻失敗,這種失敗與包含錯誤的代碼大相徑庭,是以調試起來十分困難。與此相關的另一點不足在于,JavaBeans 模式使得把類做成不可變的可能性不複存在,這就需要程式員付出額外的努力來確定它的線程安全。

幸運的是,還有第三種替代方法,它既能保證像重疊構造器模式那樣的安全性,也能保證像JavaBeans 那麼好的可讀性。這就是建造者(Builder)模式的一種形式,它不直接生成想要的對象,而是讓用戶端利用所有必要的參數調用構造器(或者靜态工廠),得到一個builder 對象。然後用戶端在builder 對象上調用類似于setter 的方法,來設定每個相關的可選參數。最後,用戶端調用無參的build 方法來生成通常是不可變的對象。這個builder 通常是它建構的類的靜态成員類,下面就是它的示例:

/**
 * 營養成分
 */
public class NutritionFacts {
    private final int servingSize; //  每份含量             required
    private final int servings; //    每罐含量     required

    private final int calories;// 卡路裡/罐      optional
    private final int fat;//   脂肪/罐              optional
    private final int sodium; //   鈉/罐         optional
    private final int carbohydrate; // 碳水/罐     optional

    private NutritionFacts(Builder builder) {
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium= builder.sodium;
        carbohydrate = builder.carbohydrate;
    } 
    
    public static class Builder{
        private int servingSize; // (ml) 每份含量             required
        private int servings; // (per container) 每罐含量     required

        private int calories;// (per serving) 卡路裡/每罐      optional
        private int fat;// (g/serving)脂肪  g/罐              optional
        private int sodium; // (mg/serving) 鈉  mg/罐         optional
        private int carbohydrate; //(g/serving) 碳水 g/罐     optional

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

注意 NutritionFacts 是不可變的,所有的預設參數值都單獨放在一個地方。builder的設定方法傳回自身,以便把調用連結起來,得到一個流式API。下面就是其用戶端代碼:

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

這樣的用戶端代碼很容易編寫,更為重要的是易于閱讀。Builder 模式模拟了可選參數

為了簡潔起見,示例中省略了有效性檢查。要想盡快偵測到無效的參數,可以在builder 的構造器和方法中檢查參數的有效性。檢視不可變量,包括build方法調用的構造器中的多個參數。為了確定這些不變量免受攻擊,從builder 複制完參數之後,要檢查對象域(詳見第50條)。如果檢查失敗就抛出 IllegalArgumentException,其中的詳細資訊會說明哪些參數是無效的。

與構造器相比,builder 的微弱優勢在于,它可以有多個可變(varargs)參數。因為builder 是利用單獨的方法來設定每一個參數。

Builder 模式的确也有它自身的不足。為了建立對象,必須先建立它的建構器。雖然建立這個建構器的開銷在實踐中可能不那麼明顯,但是在某些十分注重性能的情況下,可能就成問題了。Builder 模式還比重疊構造器模式更加冗長,是以它隻在有很多參數的時候才使用,比如4個或更多。但是記住,将來你可能需要添加參數。如果一開始就使用構造器或靜态工廠,等到類需要多個參數時才添加構造器,就會無法控制,那些過時的構造器或者靜态工廠顯得十分不協調。是以,通常最好一開始就使用建構器(Builder)。

簡而言之,如果類的構造器或者靜态工廠中具有多個參數,設計這種類時,Builder模式就是一種不錯的選擇,特别是當大多數參數都是可選或者類型相同的時候。與使用重疊構造器模式相比,使用Builder 模式的用戶端将更易于閱讀和編寫,建構器也比JavaBeans 更加安全

如果是内部調用較多,确定沒有安全問題,JavaBean模式和builder 已經非常接近了,并且,可以使用鍊式調用的方式,讓JavaBean 的調用顯得更簡潔,更像builder 

/**
 * 營養成分
 */
public class NutritionFacts {
    private int servingSize; //  每份含量             required
    private int servings; //    每罐含量     required

    private int calories;// 卡路裡/罐      optional
    private int fat;//   脂肪/罐              optional
    private int sodium; //   鈉/罐         optional
    private int carbohydrate; // 碳水/罐     optional


    public NutritionFacts setServingSize(int servingSize) {
        this.servingSize = servingSize;
        return this;
    }

    public NutritionFacts setServings(int servings) {
        this.servings = servings;
        return this;
    }

    public NutritionFacts setCalories(int calories) {
        this.calories = calories;
        return this;
    }

    public NutritionFacts setFat(int fat) {
        this.fat = fat;
        return this;
    }

    public NutritionFacts setSodium(int sodium) {
        this.sodium = sodium;
        return this;
    }

    public NutritionFacts setCarbohydrate(int carbohydrate) {
        this.carbohydrate = carbohydrate;
        return this;
    }

    public static void main(String[] args) {
        NutritionFacts cocaCola = new NutritionFacts()
                .setServingSize(240)
                .setServings(8)
                .setCalories(100)
                .setSodium(35)
                .setCarbohydrate(27);
    } 
}
           

當然,現在很多公司和團隊已經開始積極使用Lombok 來簡化bean 對象。可以如下

/**
 * 營養成分
 */
@Getter
@Setter
@Accessors(chain = true)
public class NutritionFacts {
    private int servingSize; //  每份含量             required
    private int servings; //    每罐含量     required

    private int calories;// 卡路裡/罐      optional
    private int fat;//   脂肪/罐              optional
    private int sodium; //   鈉/罐         optional
    private int carbohydrate; // 碳水/罐     optional

    public static void main(String[] args) {
        NutritionFacts cocaCola = new NutritionFacts()
                .setServingSize(240)
                .setServings(8)
                .setCalories(100)
                .setSodium(35)
                .setCarbohydrate(27);
    }
}