摘要:因此,最好一開始就考慮使用構造器。與使用傳統的重疊構造器模式相比,使用模式的客戶端代碼更易于閱讀和編寫,構建器也比更加安全。
??靜態工廠和構造器有個共同的局限性:他們都不能很好地擴展到大量的可選參數。考慮用一個類表示包裝食品外面顯示的營養成分標簽。這些標簽中有幾個域是必需的:每份的含量、每罐的含量以及每份的卡路里,還有超過20個可選域:總脂肪、飽和脂肪量、轉化脂肪、膽固醇、鈉等等。大多數產品在某幾個可選域中都會有非零的值。
??對于這樣的類,應該采用哪種構造器或者靜態方法來編寫呢?程序猿一向習慣采用重疊構造器(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個參數,看起來還不算太糟,問題是隨著參數數目的增加,它很快就失去了控制。
??總的來說,使用重疊構造器模式是可行的,但是當有很多參數的時候就很難編寫客戶端代碼,也很難去閱讀它們。如果讀者想要知道這些值代表什么意思,就必須仔細地數著這些參數來探個究竟。一長串類型相同的參數會導致一些微妙的錯誤,如果客戶端不小心顛倒了期中兩個參數的順序,編譯器也不會報錯,但是程序在運行的時候就會出現錯誤的行為。
??遇到許多構造器參數的時候,還有第二種代替方法,即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; } }
??這種模式彌補了重疊構造器模式的不足。說得明白一點,就是創建實例很容易,這樣產生的代碼讀起來也很容易。
NutritionFacts cocaCola = new NutritionFacts(); cocaCola.setServingSize(240); cocaCola.setServings(8); cocaCola.setCalories(100); cocaCola.setSodium(35); cocaCola.setCarbohydrate(27);
??遺憾的是,這種JavaBean模式自身有很嚴重的缺點。因為構造過程被分到了幾個調用中,在構造的過程中JavaBean可能處于不一致的狀態。類無法通過檢驗構造器參數的有效性來保證一致性。試圖使用處于不一致狀態的對象,將會導致失敗,這種失敗與包含錯誤的代碼大相徑庭,因此它調試起來十分困難。與此相關的另一點不足在于,JavaBean模式阻止了把類做成了不可變的可能(第17項),這就需要程序猿付出額外的努力來保證它的線程安全。
??在構造器完成構造對象之前進行加鎖,完成構造之后進行解鎖,這就能彌補以上的不足之處,但是這種方式十分笨拙,在實踐中很少使用。此外,它甚至會在運行時出現錯誤,因為編譯器無法確保程序猿會在使用構造器之前進行加鎖操作。
??幸運的是,還有第三種替代方法,結合了重疊構造器的安全性和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是不可變的,所有默認參數值都多帶帶放在一個地方。builder的setter方法返回的是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 Settoppings; abstract static class Builder > { EnumSet 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{ 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 { 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更加安全。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/73875.html
摘要:本章中的大部分內容適用于構造函數和方法。第項其他方法優先于序列化第項謹慎地實現接口第項考慮使用自定義的序列化形式第項保護性地編寫方法第項對于實例控制,枚舉類型優先于第項考慮用序列化代理代替序列化實例附錄與第版中項目的對應關系參考文獻 effective-java-third-edition 介紹 Effective Java 第三版全文翻譯,純屬個人業余翻譯,不合理的地方,望指正,感激...
摘要:并沒有類繼承模型,而是使用原型對象進行原型式繼承。我們舉例說明原型鏈查找機制當訪問一個對象的屬性時,會從對象本身開始往上遍歷整個原型鏈,直到找到對應屬性為止。原始類型有以下五種型。此外,試圖查找一個不存在屬性時將會遍歷整個原型鏈。 Javascript 并沒有類繼承模型,而是使用原型對象 prototype 進行原型式繼承。 盡管人們經常將此看做是 Javascript 的一個缺點,然...
摘要:推薦序前言致謝第一章引言第二章創建和銷毀對象第項用靜態工廠方法代替構造器第項遇到多個構造器參數時要考慮使用構建器第項用私有構造器或者枚舉類型強化屬性第項通過私有構造器強化不可實例化的能力第項優先考慮依賴注入來引用資源第項避免創建不必要的對象 推薦序 前言 致謝 第一章 引言 第二章 創建和銷毀對象 第1項:用靜態工廠方法代替構造器 第2項:遇到多個構造器參數時要考慮使用構建器 第...
摘要:一個類可以提供一個公共靜態工廠方法,它僅僅是一第項遇到多個構造器參數時要考慮使用構建器靜態工廠和構造器有個共同的局限性他們都不能很好地擴展到大量的可選參數。 ??本章涉及創建和銷毀對象,包括何時以及如何創建它們,何時以及如何避免創建它們,如何確保它們被及時銷毀,以及如何管理在銷毀之前必須進行的清理操作。 第1項:用靜態工廠方法代替構造器 ??類允許客戶端獲取實例的傳統方法是提供公共構造...
閱讀 1265·2021-09-27 13:35
閱讀 2563·2021-09-06 15:12
閱讀 3380·2019-08-30 15:55
閱讀 2829·2019-08-30 15:43
閱讀 432·2019-08-29 16:42
閱讀 3446·2019-08-29 15:39
閱讀 3062·2019-08-29 12:28
閱讀 1239·2019-08-29 11:11