文章作者:Tyan
博客:noahsnail.com
Item 2: Consider a builder when faced with many constructor parameters
Item 2:當(dāng)面臨很多構(gòu)造函數(shù)參數(shù)時(shí)蕾域,要考慮使用構(gòu)建器
Static factories and constructors share a limitation: they do not scale well to large numbers of optional parameters. Consider the case of a class representing the Nutrition Facts label that appears on packaged foods. These labels have a few required fields—serving size, servings per container, and calories per serving and over twenty optional fields—total fat, saturated fat, trans fat, cholesterol, sodium, and so on. Most products have nonzero values for only a few of these optional fields.
靜態(tài)工廠和構(gòu)造函數(shù)有一個(gè)共同的限制:對于大量可選參數(shù)它們都不能很好的擴(kuò)展忧勿〗招唬考慮這樣一種情況:用一個(gè)類來表示包裝食品上的營養(yǎng)成分標(biāo)簽。這些標(biāo)簽有幾個(gè)字段是必須的——每份含量、每罐含量(份數(shù))怀酷、每份的卡路里,二十個(gè)以上的可選字段——總脂肪量、飽和脂肪量猪杭、轉(zhuǎn)化脂肪、膽固醇妥衣、鈉等等皂吮。大多數(shù)產(chǎn)品中這些可選字段中的僅有幾個(gè)是非零值。
What sort of constructors or static factories should you write for such a class? Traditionally, programmers have used the telescoping constructor pattern, in which you provide a constructor with only the required parameters, another with a single optional parameter, a third with two optional parameters, and so on, culminating in a constructor with all the optional parameters. Here’s how it looks in practice. For brevity’s sake, only four optional fields are shown:
你應(yīng)該為這樣的一個(gè)類寫什么樣的構(gòu)造函數(shù)或靜態(tài)工廠税手?習(xí)慣上蜂筹,程序員使用重疊構(gòu)造函數(shù)模式,在這種模式中只給第一個(gè)構(gòu)造函數(shù)提供必要的參數(shù)芦倒,給第二個(gè)構(gòu)造函數(shù)提供一個(gè)可選參數(shù)艺挪,給第三個(gè)構(gòu)造函數(shù)提供兩個(gè)可選參數(shù),以此類推兵扬,最后的構(gòu)造函數(shù)具有所有的可選參數(shù)麻裳。下面是一個(gè)實(shí)踐中的例子。為了簡便器钟,只顯示了四個(gè)可選字段:
//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; // optional
private final int fat; // (g) optional
private final int sodium; // (mg) optional
private final int carbohydrate; // (g) 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;
}
}
When you want to create an instance, you use the constructor with the shortest parameter list containing all the parameters you want to set:
當(dāng)你想創(chuàng)建一個(gè)實(shí)例時(shí)津坑,你可以使用具有最短參數(shù)列表的構(gòu)造函數(shù),最短參數(shù)列表包含了所有你想設(shè)置的參數(shù):
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
Typically this constructor invocation will require many parameters that you don’t want to set, but you’re forced to pass a value for them anyway. In this case, we passed a value of 0 for fat. With “only” six parameters this may not seem so bad, but it quickly gets out of hand as the number of parameters increases.
通常構(gòu)造函數(shù)調(diào)用需要許多你不想設(shè)置的參數(shù)傲霸,但無論如何你不得不為它們傳值国瓮。在這種情況下,我們給fat
傳了一個(gè)零值狞谱。只有六個(gè)參數(shù)可能還不是那么糟糕乃摹,但隨著參數(shù)數(shù)目的增長它很快就會失控。
In short, **the telescoping constructor pattern works, but it is hard to write client code when there are many parameters, and harder still to read it. **The reader is left wondering what all those values mean and must carefully count parameters to find out. Long sequences of identically typed parameters can cause subtle bugs. If the client accidentally reverses two such parameters, the compiler won’t complain, but the program will misbehave at runtime (Item 40).
簡而言之跟衅,重疊構(gòu)造函數(shù)模式有作用孵睬,但是當(dāng)有許多參數(shù)時(shí)很難編寫客戶端代碼,更難的是閱讀代碼伶跷。讀者會很奇怪所有的這些值是什么意思掰读,必須仔細(xì)的計(jì)算參數(shù)個(gè)數(shù)才能查明秘狞。一長串同類型的參數(shù)會引起細(xì)微的錯(cuò)誤。如果客戶端偶然的顛倒了兩個(gè)這樣的參數(shù)蹈集,編譯器不會報(bào)錯(cuò)烁试,但程序在運(yùn)行時(shí)會出現(xiàn)錯(cuò)誤的行為(Item 40)。
A second alternative when you are faced with many constructor parameters is the JavaBeans pattern, in which you call a parameterless constructor to create the object and then call setter methods to set each required parameter and each optional parameter of interest:
當(dāng)你面臨許多構(gòu)造函數(shù)參數(shù)時(shí)拢肆,第二個(gè)替代選擇是JavaBeans模式减响,在這種模式中你要調(diào)用無參構(gòu)造函數(shù)來創(chuàng)建對象,然后調(diào)用setter
方法為每一個(gè)必要參數(shù)和每一個(gè)有興趣的可選參數(shù)設(shè)置值:
//JavaBeans Pattern - allows inconsistency, mandates mutability
public class NutritionFacts {
// Parameters initialized to default values (if any) private int servingSize
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;
}
}
This pattern has none of the disadvantages of the telescoping constructor pattern. It is easy, if a bit wordy, to create instances, and easy to read the resulting code:
這個(gè)模式?jīng)]有重疊構(gòu)造函數(shù)模式的缺點(diǎn)郭怪。即使有點(diǎn)啰嗦支示,但它很容易創(chuàng)建實(shí)例,也很容易閱讀寫出來的代碼:
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
Unfortunately, the JavaBeans pattern has serious disadvantages of its own. Because construction is split across multiple calls, a JavaBean may be in an inconsistent state partway through its construction. The class does not have the option of enforcing consistency merely by checking the validity of the constructor parameters. Attempting to use an object when it’s in an inconsistent state may cause failures that are far removed from the code containing the bug, hence difficult to debug. A related disadvantage is that the JavaBeans pattern precludes the possibility of making a class immutable (Item 15), and requires added effort on the part of the programmer to ensure thread safety.
遺憾的是鄙才,JavaBeans模式自身有著嚴(yán)重缺點(diǎn)颂鸿。因?yàn)闃?gòu)造過程跨越多次調(diào)用,JavaBean在構(gòu)造過程中可能會出現(xiàn)不一致的狀態(tài)攒庵。JavaBean類不能只通過檢查構(gòu)造函數(shù)參數(shù)的有效性來保證一致性嘴纺。當(dāng)一個(gè)對象處于一種不一致的狀態(tài)時(shí),試圖使用它可能會引起失敗浓冒,這個(gè)失敗很難從包含錯(cuò)誤的代碼中去掉颖医,因此很難調(diào)試。與此相關(guān)的一個(gè)缺點(diǎn)是JavaBeans模式排除了使一個(gè)類不可變的可能性*(Item 15)裆蒸,因此需要程序員付出額外的努力來確保線程安全熔萧。
It is possible to reduce these disadvantages by manually “freezing” the object when its construction is complete and not allowing it to be used until frozen, but this variant is unwieldy and rarely used in practice. Moreover, it can cause errors at runtime, as the compiler cannot ensure that the programmer calls the freeze method on an object before using it.
當(dāng)構(gòu)造工作完成時(shí),可以通過手動『冰凍』對象并且在冰凍完成之前不允許使用它來彌補(bǔ)這個(gè)缺點(diǎn)僚祷,但這種方式太笨重了佛致,在實(shí)踐中很少使用。而且辙谜,由于編譯器不能保證程序員在使用對象之前調(diào)用了冰凍方法俺榆,因此它可能在運(yùn)行時(shí)引起錯(cuò)誤。
Luckily, there is a third alternative that combines the safety of the telescoping constructor pattern with the readability of the JavaBeans pattern. It is a form of the Builder pattern [Gamma95, p. 97]. Instead of making the desired object directly, the client calls a constructor (or static factory) with all of the required parameters and gets a builder object. Then the client calls setter-like methods on the builder object to set each optional parameter of interest. Finally, the client calls a parameterless build method to generate the object, which is immutable. The builder is a static member class (Item 22) of the class it builds. Here’s how it looks in practice:
幸運(yùn)的是装哆,這兒還有第三種替代方法罐脊,它結(jié)合了重疊構(gòu)造函數(shù)模式的安全性和JavaBeans模式的可讀性。它就是構(gòu)建器模式[Gamma95蜕琴, p. 97]萍桌。它不直接構(gòu)建需要的對象,客戶端調(diào)用具有所有參數(shù)的構(gòu)造函數(shù)(或靜態(tài)工廠)凌简,得到一個(gè)構(gòu)造器對象上炎。然后客戶端在構(gòu)建器上調(diào)用類似于setter的方法來設(shè)置每個(gè)感興趣的可選參數(shù)。最終雏搂,客戶端調(diào)用無參構(gòu)建方法來產(chǎn)生一個(gè)對象藕施,這個(gè)對象是不可變的寇损。構(gòu)建器是它要構(gòu)建的類的靜態(tài)成員類(Item 22)。它在實(shí)踐中的形式如下:
//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 carbohydrate = 0;
private int sodium = 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 carbohydrate(int val) {
carbohydrate = val;
return this;
}
public Builder sodium(int val) {
sodium = 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;
}
}
Note that NutritionFacts
is immutable, and that all parameter default values are in a single location. The builder’s setter methods return the builder itself so that invocations can be chained. Here’s how the client code looks:
注意NutritionFacts
是不可變的裳食,所有參數(shù)的默認(rèn)值都在一個(gè)單獨(dú)的位置矛市。構(gòu)建器的setter
方法返回的是構(gòu)建器本身,為的是可以鏈?zhǔn)秸{(diào)用诲祸∽抢簦客戶端代碼如下:
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).carbohydrate(27).build();
This client code is easy to write and, more importantly, to read. The Builder pattern simulates named optional parameters as found in Ada and Python.
Like a constructor, a builder can impose invariants on its parameters. The build method can check these invariants. It is critical that they be checked after copying the parameters from the builder to the object, and that they be checked on the object fields rather than the builder fields (Item 39). If any invariants are violated, the build method should throw an IllegalStateException
(Item 60). The exception’s detail method should indicate which invariant is violated (Item 63).
客戶端代碼很容器寫,更重要的是很容易讀烦绳。構(gòu)建器模式模擬了命名可選參數(shù),就像Ada和Python中的一樣配紫。類似于構(gòu)造函數(shù)径密,構(gòu)造器可以對它參數(shù)加上約束條件。構(gòu)造器方法可以檢查這些約束條件躺孝。將參數(shù)從構(gòu)建器拷貝到對象中之后享扔,可以在對象作用域而不是構(gòu)造器作用域?qū)s束條件進(jìn)行檢查,這是很關(guān)鍵的(Item 39)植袍。如果違反了任何約束條件惧眠,構(gòu)造器方法會拋出IllegalStateException
異常(Item 60)。異常的詳細(xì)信息會指出違反了哪一個(gè)約束條件(Item 63)于个。
Another way to impose invariants involving multiple parameters is to have setter methods take entire groups of parameters on which some invariant must hold. If the invariant isn’t satisfied, the setter method throws an IllegalArgumentException
. This has the advantage of detecting the invariant failure as soon as the invalid parameters are passed, instead of waiting for build
to be invoked.
給許多參數(shù)加上約束條件的另一種方式是對某些約束條件必須持有的整組參數(shù)用setter方法進(jìn)行檢查氛魁,如果沒有滿足約束條件,setter方法會拋出IllegalArgumentException
異常厅篓。這個(gè)優(yōu)點(diǎn)在于是一旦傳遞了無效參數(shù)秀存,檢測約束條件會失敗,而不是等待build
被調(diào)用羽氮。
A minor advantage of builders over constructors is that builders can have multiple varargs parameters. Constructors, like methods, can have only one varargs parameter. Because builders use separate methods to set each parameter, they can have as many varargs parameters as you like, up to one per setter method.
相比于構(gòu)造函數(shù)或链,構(gòu)建器的一個(gè)小優(yōu)勢在與構(gòu)建器可以有許多可變參數(shù)。構(gòu)造函數(shù)類似于方法档押,只能有一個(gè)可變參數(shù)澳盐。由于構(gòu)造器用單獨(dú)的方法設(shè)置每一個(gè)參數(shù),因此像你喜歡的那樣令宿,它們能有許多可變參數(shù)叼耙,直到每個(gè)setter方法都有一個(gè)可變參數(shù)。
The Builder pattern is flexible. A single builder can be used to build multiple objects. The parameters of the builder can be tweaked between object creations to vary the objects. The builder can fill in some fields automatically, such as a serial number that automatically increases each time an object is created.
構(gòu)建器模式是靈活的粒没。一個(gè)構(gòu)建器可以用來構(gòu)建多個(gè)對象旬蟋。為了改變對象,構(gòu)建器參數(shù)在創(chuàng)建對象時(shí)可以進(jìn)行改變革娄。構(gòu)建器能自動填充一些字段倾贰,例如每次創(chuàng)建對象時(shí)序號自動增加冕碟。
A builder whose parameters have been set makes a fine Abstract Factory [Gamma95, p. 87]. In other words, a client can pass such a builder to a method to enable the method to create one or more objects for the client. To enable this usage, you need a type to represent the builder. If you are using release 1.5 or a later release, a single generic type (Item 26) suffices for all builders, no matter what type of object they’re building:
設(shè)置了參數(shù)的構(gòu)建器形成了一個(gè)很好的抽象工廠[Gamma95,p.87]匆浙。換句話說安寺,為了使某個(gè)方法能為客戶端創(chuàng)建一個(gè)或多個(gè)對象,客戶端可以傳遞這樣的一個(gè)構(gòu)建器到這個(gè)方法中首尼。為了使這個(gè)用法可用挑庶,你需要用一個(gè)類型來表示構(gòu)建器。如果你在使用JDK 1.5或之后的版本软能,只要一個(gè)泛型就能滿足所有的構(gòu)建器(Item 26)迎捺,無論正在構(gòu)建的是什么類型:
// A builder for objects of type T
public interface Builder<T> {
public T build();
}
Note that our NutritionFacts.Builder
class could be declared to implement Builder<NutritionFacts>
.
注意我們可以聲明NutritionFacts.Builder
類來實(shí)現(xiàn)Builder<NutritionFacts>
。
Methods that take a Builder instance would typically constrain the builder’s type parameter using a bounded wildcard type (Item 28). For example, here is a method that builds a tree using a client-provided Builder instance to build each node:
帶有構(gòu)建器實(shí)例的方法通常使用綁定的通配符類型來約束構(gòu)建器的類型參數(shù)(Item 28)查排。例如凳枝,構(gòu)建樹的方法通過使用客戶端提供的構(gòu)建器實(shí)例來構(gòu)建每一個(gè)結(jié)點(diǎn):
Tree buildTree(Builder<? extends Node> nodeBuilder) { ... }
The traditional Abstract Factory implementation in Java has been the Class object, with the newInstance
method playing the part of the build
method. This usage is fraught with problems. The newInstance
method always attempts to invoke the class’s parameterless constructor, which may not even exist. You don’t get a compile-time error if the class has no accessible parameterless constructor. Instead, the client code must cope with InstantiationException
or IllegalAccessException
at runtime, which is ugly and inconvenient. Also, the newInstance
method propagates any exceptions thrown by the parameterless constructor, even though newInstance
lacks the corresponding throws clauses. In other words, Class.newInstance
breaks compile-time exception checking. The Builder
interface, shown above, corrects these deficiencies.
Java中傳統(tǒng)的抽象工廠實(shí)現(xiàn)是類對象,newInstance
方法扮演著build
方法的角色跋核。 這種用法問題重重岖瑰。newInstance
方法總是嘗試調(diào)用類的無參構(gòu)造函數(shù),但無參構(gòu)造函數(shù)可能并不存在砂代。如果類沒有訪問無參構(gòu)造函數(shù)蹋订,你不會收到編譯時(shí)錯(cuò)誤。而客戶端代碼必須處理運(yùn)行時(shí)的InstantiationException
或IllegalAccessException
異常刻伊,這樣既不雅觀也不方便露戒。newInstance
也會傳播無參構(gòu)造函數(shù)拋出的任何異常,即使newInstance
缺少對應(yīng)的拋出語句塊捶箱。換句話說玫锋,Class.newInstance
打破了編譯時(shí)的異常檢測。上面的Builder
接口彌補(bǔ)了這些缺陷讼呢。
The Builder pattern does have disadvantages of its own. In order to create an object, you must first create its builder. While the cost of creating the builder is unlikely to be noticeable in practice, it could be a problem in some performance-critical situations. Also, the Builder pattern is more verbose than the telescoping constructor pattern, so it should be used only if there are enough parameters, say, four or more. But keep in mind that you may want to add parameters in the future. If you start out with constructors or static factories, and add a builder when the class evolves to the point where the number of parameters starts to get out of hand, the obsolete constructors or static factories will stick out like a sore thumb. Therefore, it’s often better to start with a builder in the first place.
構(gòu)建器模式也有它的缺點(diǎn)撩鹿。為了創(chuàng)建對象,你必須首先創(chuàng)建它的構(gòu)建器悦屏。雖然創(chuàng)建構(gòu)建器的代價(jià)在實(shí)踐中可能不是那么明顯节沦,但在某些性能優(yōu)先關(guān)鍵的情況下它可能是一個(gè)問題。構(gòu)建器模式比重疊構(gòu)造函數(shù)模式更啰嗦础爬,因此只有在參數(shù)足夠多的情況下才去使用它甫贯,比如四個(gè)或更多。但要記住將來你可能會增加參數(shù)看蚜。如果你開始使用構(gòu)造函數(shù)或靜態(tài)工廠叫搁,當(dāng)類發(fā)展到參數(shù)數(shù)目開始失控的情況下,才增加一個(gè)構(gòu)建器,廢棄的構(gòu)造函數(shù)或靜態(tài)工廠就像一個(gè)疼痛的拇指渴逻,最好是在開始就使用構(gòu)建器疾党。
In summary, the Builder pattern is a good choice when designing classes whose constructors or static factories would have more than a handful of parameters, especially if most of those parameters are optional. Client code is much easier to read and write with builders than with the traditional telescoping constructor pattern, and builders are much safer than JavaBeans.
總之,當(dāng)設(shè)計(jì)的類的構(gòu)造函數(shù)或靜態(tài)工廠有許多參數(shù)時(shí)惨奕,構(gòu)建器模式是一個(gè)很好的選擇雪位,尤其是大多數(shù)參數(shù)是可選參數(shù)的情況下。與傳統(tǒng)的重疊構(gòu)造函數(shù)模式相比梨撞,使用構(gòu)建器模式的客戶端代碼更易讀易編寫雹洗,與JavaBeans模式相比使用構(gòu)建器模式更安全。