effective java 3th item2:考慮 builder 模式,當構造器參數過多的時候

yiaz 讀書筆記,翻譯於 effective java 3th 英文版,可能有些地方有錯誤。歡迎指正。java

靜態工廠方法和構造器都有一個限制:當有許多參數的時候,它們不能很好的擴展。程序員

好比試想下以下場景:考慮使用一個類表示食品包裝袋上的養分成分標籤。這些標籤只有幾個是必須的——每份的含量、每罐的含量、每份的卡路里,除了這幾個必選的,還有超過 20 個可選的標籤——總脂肪量、飽和脂肪量等等。對於這些可選的標籤,大部分產品通常都只有幾個標籤的有值,不是每個標籤都用到。安全

  1. telescoping constructor重疊構造器模式ide

    對於這種狀況,你應該選擇哪一種構造器或者靜態工廠方法。通常程序員的習慣是採用 (telescoping constructor重疊構造器模式。在這種模式中,提供一個包含必選參數的構造器,再提供其餘一些列包含可選參數的構造器,第一個包含一個能夠參數、第二個包含兩個可選參數,以此類推下去,直到包含全部的可選參數。性能

    示例代碼:ui

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

    當你想建立一個實例的時候,你只須要找包含你須要的而且是最短參數列表的構造器便可。this

    這裏有一些問題,好比看下面的代碼:翻譯

    NutritionFacts cocaCola = new NutritionFacts(240, 8, 0, 0, 35, 27);設計

    其中,第 1,2 個可選參數,咱們是不須要的,可是程序中沒有提供直接賦值第 3,4個可選參數的構造器,所以,咱們只能選擇包含了 1,2,3,4 個參數的構造器。這裏面要求了許多你不想設置的參數,可是你又被迫的設置它們,在這裏,傳入對應的屬性的默認值 0。而且這種模式,隨着參數的增長,將變得愈來愈難以忍受,不管是編寫程序的人,仍是調用程序的人。code

    總而言之,(telescoping constructor重疊構造器模式,可使用,可是它對客戶端來講,很不友好,寫和讀都是一件困難的事情。它們很難搞懂那些參數對應的究竟是什麼屬性,必須好好的比對構造器代碼。而且當參數不少的時候,很容易出 bug,若是使用的時候,無心間顛倒了兩個參數的位置,編譯器是不會出現警告的,由於這裏的類型同樣,都是 int ,直到運行的時候纔會暴露出。

  2. Javabeans 模式

    咱們還有一種選擇,使用 Javabeans 模式

    在此模式中,咱們提供一個 無參構造器 建立實例,而後利用 setXXX 方法,設置每個必須的屬性和每個須要的可選屬性。

    示例代碼:

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

    Javabeans 模式,沒有 重疊構造器模式 的缺點,對於冗長的參數,使用它建立對象,會很容易,同時讀起來也是容易。正以下面看到的,咱們能夠清晰的看到,每個屬性的值。

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

    不幸運的是,Javabeans模式 自己有着嚴重的缺點:由於,建立對象被分割爲多個步驟,先是利用無參構造器建立對象,而後再依次設置屬性。這致使一個問題: Javabean 在其建立過程當中,可能處於不一致1的狀態。 類不能經過檢查構造器的參數,來保證對象的一致性。

    另一個缺點是,將建立一個可變的類的難度提升了好幾個級別,由於有 setXXX 方法的存在。

    能夠經過一些手段來減小不一致的問題,經過一些手段 凍結 對象,在對象被建立完成以前。而且不容許使用該對象,直到 解凍 。可是這種方式很是笨拙,在實踐中不多使用。由於,編譯器沒法確認程序員在使用一個對象以前,該對象是否已經 解凍

  3. Builder 模式

    幸運的是,這裏還有一種方法 Builder 模式,兼顧 重疊構造器 的安全以及 Javabean模式 的可讀性。

    客戶端先經過調用構造器或者靜態工廠方法,傳入必須的參數,得到一個 builder 對象,代替直接獲取目標對象。而後客戶端在該 builder 對象上調用 setXXX 方法,爲每個感興趣的可選屬性賦值,最後客戶端調用一個 無參構造器 生成最終的目標對象,該對象通常是不可變的。其中 Builder 類是目標類的靜態內部類

    示例代碼:

    // 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 類爲不可變類,類的成員變量所有被 final 修飾,參數的默認值被放在一個地方。BuildersetXXX 方法返回 Builder 自己,這種寫法,能夠將設置變成一個鏈,一直點下去(fluent APIs):

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

    這樣的客戶端代碼,容易編寫,更容易閱讀。

    示例代碼中,爲了簡潔,省去了有效性的檢查。通常,爲了儘快的檢查到非法參數,咱們在 builder 的構造器和方法中,對其參數進行檢查。

    還須要檢查 build 方法中調用的構造器的多個不可變參數2。此次檢查延遲到 object 中,爲了確保這些不可變參數不受到攻擊,在 builder 將屬性複製到 object 中的時候,再作一次檢查。若是檢驗失敗,則拋出 IllegalArgumentException 異常,異常信息中提示哪些參數不合法。

    Bulider 模式很適合類的層次結構。可使用一個 builder 的平行結構,即每個 builder 嵌套在一個對應的類中,抽象類中有抽象的 builder ,具體類中有具體的 builder 。像下面的代碼所示:

    // Builder pattern for class hierarchies
        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
            }
        }
    
        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;
            }
        }
    
        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;
            }
        }

    注意,這裏的 Pizza.Builder 是類屬性,被 static 修飾的,而且泛型參數,是一個 遞歸 的泛型參數,繼承自己。和返回自身的抽象方法 self ,搭配一塊兒,能夠鏈式的調用下去,不須要進行類型的轉換,這樣作的緣由是,java 不直接支持 自類型 3,能夠模擬自類型 4

    若是不使用模擬自類型的話,調用 addTopping方法,返回的其實就是抽象類中的 Builder ,這樣就致使沒法調用子類擴展方法,沒法使用 fluent APIS。其中 build 方法,使用了 1.5 添加的 協變類型 ,它能夠不用 cast 轉換,就直接使用具體的類型,不然子類接收父類,是須要強轉的 。

    builder 模式另一個小優勢:builder 能夠有多個 可變參數,由於,能夠將多個可變參數,放到各自對應的方法中5。另外 build 能夠將多個參數合併到一個字段上,就如上面代碼中 addTopping 的那樣6

    builder 模式是很是靈活的。一個單一的 builder 屢次調用,能夠建立出不一樣的對象7builder 的參數,能夠在調用 build 方法的時候進行細微調整,以便修改建立出的對象8builder 模式還能夠自動的填充 object域的字段在建立對象的時候。好比爲每一個新建立的對象設置編號,只須要在 builder 中維護一個類變量便可。

    builder 模式也是有缺點的。爲了建立一個對象,咱們首先須要建立它的 builder 對象。雖然,建立 builder 對象的開銷,在實踐中不是很明顯,可是在對性能要求很嚴格的場景下,這種開銷能會成爲一個問題。同時,builder 模式是很是冗雜的,對於比 重疊構造器 ,因此,builder 模式應該僅僅被用在構造器參數足夠多的狀況下,好比三個、四個或者更多,只有這樣,使用 builder 模式纔是值得的。可是,你要時刻記住,類在未來可能會添加新的參數,若是你一開始使用了構造器或者靜態工廠方法,隨着類的變化,類的屬性參數變得足夠多,這時候你再切換到 builder 模式,那麼一開始的構造器和靜態工廠方法就會被廢棄,這些廢棄的方法看起來很凸出,你還不能刪除它們,須要保存兼容性。所以,通常一開始就選擇 builder 模式是一個不錯的選擇。

    總結,builder 模式是一個好的選擇,當設計一個類的時候,該類的構造器參數或者靜態工廠參數不止幾個參數,尤爲是許多參數是可選的或者同一個類型(可變參數)。這樣設計的類,客戶端代碼,與靜態工廠方法和重疊構造器比起來更加容易閱讀和編寫,和 Javabeans 模式比起來更加安全。



  1. 不一致的意思:正常對象的建立應該是一個完整的過程,這個過程控制在構造器中,能夠看作是一個 原子性 的操做。它在對象建立出來之後,對象的各項屬性已經被正確的初始化。可是 Javabean 模式,天生的背棄了這個原則,它的建立對象,不是一個 原子性 的操做,在構造器執行完畢之後,還有一些列的屬性賦值,在這期間任何引用該對象的地方,都將得到一個不正確的對象,直到對象建立完畢。能夠參考下 JavaBean disadvantage - inconsistent during construction 這裏還提到了重複錯誤對象的建立。

  2. 我理解爲構造器所在類的不可變屬性,在 builder 中的檢查相似於前臺頁面字段的合法性檢查,最後後臺(Object)都要再次檢查一遍。

  3. 自類型 。在支持自類型的語言中,this 或者 self 的語義,誰調用該方法,則 this 表明誰。可是在 java 中,方法中的 this 指代的是定義該方法的類型,與調用無關,致使沒法很好的使用 fluent API。可參考 java 的 self 類型。你能夠驗證下,打印控制檯看,類型確實是調用它的類型,可是你等號左邊用這個類型去接收,會提示你發現父類型,不能賦值給子類型,不知道 java 在這裏面作了什麼。

  4. 模擬自類型,這裏在抽象類中,使用泛型指定,避免使用指定的類型,致使 this 被綁定爲具體的。

  5. 構造器在建立對象的時候,構造器和普通方法同樣,只能接受一個 可變參數 。可是 builder 模式,能夠屢次調用不一樣的方法,添加 可變參數,直到全部的可變參數所有添加完畢,再 build 建立對象。

  6. 一樣的,構造器沒法作的緣由是,構造器一經調用,對象就會被建立,也就是建立對象的過程當中,只能夠調用一次構造器。可是 builder 模式能夠屢次調用方法,設置參數,直到最後所有添加完畢,調用 build 建立對象。

  7. 仍是由於 builder 模式,只有在調用 build 方法,對象纔會被建立,在建立以前,能夠在調用 builder 模式的方法,修改參數,建立出不一樣的對象。 能夠參考下 StackOverflow 的回答: A single builder can be used repeatedly to build multiple objects

  8. 仍是由於 builder 模式,只有在調用 build 方法,對象纔會被建立,在建立以前,能夠在調用 builder 模式的方法,修改參數,建立出不一樣的對象。 能夠參考下 StackOverflow 的回答: A single builder can be used repeatedly to build multiple objects

相關文章
相關標籤/搜索