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 more than 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ù)都有一個局限:它們不能對大量可選參數(shù)做很好的擴展。以一個類為例青团,它表示包裝食品上的營養(yǎng)標簽恩掷。這些標簽上有一些字段是必需的,如:凈含量特漩、毛重和每單位份量的卡路里,另有超過 20 個可選的字段骨杂,如:總脂肪拾稳、飽和脂肪、反式脂肪腊脱、膽固醇访得、鈉等等。大多數(shù)產(chǎn)品只有這些可選字段中的少數(shù)陕凹,且具有非零值悍抑。
What sort(n.種類,方式杜耙;vt.將…排序) 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òu)造函數(shù)或靜態(tài)工廠呢搜骡?傳統(tǒng)的方式是使用可伸縮構(gòu)造函數(shù),在這種模式中佑女,只向構(gòu)造函數(shù)提供必需的參數(shù)记靡。即,向第一個構(gòu)造函數(shù)提供單個可選參數(shù)团驱,向第二個構(gòu)造函數(shù)提供兩個可選參數(shù)摸吠,以此類推,最后一個構(gòu)造函數(shù)是具有所有可選參數(shù)的嚎花。這是它在實際應(yīng)用中的樣子寸痢。為了簡潔起見,只展示具備四個可選字段的情況:
// 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;
}
}
When you want to create an instance, you use the constructor with the shortest parameter list containing all the parameters you want to set:
當你想要創(chuàng)建一個實例時紊选,可以使用包含所需的參數(shù)的最短參數(shù)列表的構(gòu)造函數(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ù)包含許多額外的參數(shù),但是你必須為它們傳遞一個值兵罢。在本例中献烦,我們?yōu)?fat 傳遞了一個值 0。只有六個參數(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 51).
簡單地說拢操,可伸縮構(gòu)造函數(shù)模式可以工作,但是當有很多參數(shù)時舶替,編寫客戶端代碼是很困難的令境,而且讀起來更困難。 讀者想知道所有這些值是什么意思顾瞪,必須仔細清點參數(shù)舔庶。相同類型參數(shù)的長序列會導致細微的錯誤。如果客戶端不小心倒轉(zhuǎn)了兩個這樣的參數(shù)陈醒,編譯器不會報錯惕橙,但是程序會在運行時出錯(Item-51)。
A second alternative(n.二中擇一钉跷;adj.供選擇的) when you’re faced with many optional parameters in a constructor 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:
當你在構(gòu)造函數(shù)中遇到許多可選參數(shù)時弥鹦,另一種選擇是 JavaBean 模式,在這種模式中爷辙,你調(diào)用一個無參數(shù)的構(gòu)造函數(shù)來創(chuàng)建對象彬坏,然后調(diào)用 setter 方法來設(shè)置每個所需的參數(shù)和每個感興趣的可選參數(shù):
// 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; }
}
This pattern has none of the disadvantages of the telescoping constructor pattern.It is easy, if a bit wordy(adj.冗長的), to create instances, and easy to read the resulting(v.產(chǎn)生;adj.作為結(jié)果的) code:
這個模式?jīng)]有可伸縮構(gòu)造函數(shù)模式的缺點膝晾。創(chuàng)建實例很容易栓始,雖然有點冗長,但很容易閱讀生成的代碼:
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(vt.分離血当,分解) 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(adj. 不一致的) state may cause failures that are far removed from the code containing the bug and hence(adv.因此) difficult to debug. A related disadvantage is that the JavaBeans pattern precludes the possibility of making a class immutable (Item 17) and requires added effort on the part of the programmer to ensure thread safety.
不幸的是幻赚,JavaBean 模式本身有嚴重的缺點。因為構(gòu)建是在多個調(diào)用之間進行的臊旭,所以 JavaBean 可能在構(gòu)建的過程中處于不一致的狀態(tài)落恼。該類不能僅通過檢查構(gòu)造函數(shù)參數(shù)的有效性來強制一致性。在不一致的狀態(tài)下嘗試使用對象可能會導致錯誤的發(fā)生离熏,而包含這些錯誤的代碼很難調(diào)試领跛。一個相關(guān)的缺點是,JavaBean 模式排除了使類不可變的可能性(Item-17)撤奸,并且需要程序員額外的努力來確保線程安全吠昭。
It is possible to reduce these disadvantages by manually(adv.手動地) “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 because the compiler cannot ensure that the programmer calls the freeze method on an object before using it.
通過在對象構(gòu)建完成時手動「凍結(jié)」對象,并在凍結(jié)之前不允許使用對象胧瓜,可以減少這些缺陷矢棚,但是這種變通方式很笨拙,在實踐中很少使用府喳。此外蒲肋,它可能在運行時導致錯誤,因為編譯器不能確保程序員在使用對象之前調(diào)用它的 freeze 方法。
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]. 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 typically immutable. The builder is typically a static member class (Item 24) of the class itbuilds. Here’s how it looks in practice:
幸運的是兜粘,還有第三種選擇申窘,它結(jié)合了可伸縮構(gòu)造函數(shù)模式的安全性和 JavaBean 模式的可讀性。它是建造者模式的一種形式[Gamma95]孔轴√攴ǎ客戶端不直接生成所需的對象,而是使用所有必需的參數(shù)調(diào)用構(gòu)造函數(shù)(或靜態(tài)工廠)路鹰,并獲得一個 builder 對象贷洲。然后,客戶端在構(gòu)建器對象上調(diào)用像 setter 這樣的方法來設(shè)置每個感興趣的可選參數(shù)晋柱。最后优构,客戶端調(diào)用一個無參數(shù)的構(gòu)建方法來生成對象,這通常是不可變的雁竞。構(gòu)建器通常是它構(gòu)建的類的靜態(tài)成員類(Item-24)钦椭。下面是它在實際應(yīng)用中的樣子:
// 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;
}
}
The NutritionFacts class is immutable, and all parameter default values are in one place. The builder’s setter methods return the builder itself so that invocations can be chained, resulting in a fluent API. Here’s how the client code looks:
NutritionFacts 類是不可變的,所有參數(shù)默認值都在一個位置碑诉。構(gòu)建器的 setter 方法返回構(gòu)建器本身玉凯,這樣就可以鏈接調(diào)用,從而得到一個流暢的 API联贩。下面是客戶端代碼的樣子:
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();
This client code is easy to write and, more importantly, easy to read. The Builder pattern simulates named optional parameters as found in Python and Scala.
該客戶端代碼易于編寫漫仆,更重要的是易于閱讀。建造者模式模擬 Python 和 Scala 中的可選參數(shù)泪幌。
Validity(n.有效性) checks were omitted(v.遺漏盲厌,省略;adj.省去的) for brevity(n.簡潔). To detect invalid parameters as soon as possible, check parameter validity in the builder’s constructor and methods.Check invariants involving multiple parameters in the constructor invoked by the build method. To ensure these invariants against attack, do the checks on object fields after copying parameters from the builder (Item 50). If a check fails, throw an IllegalArgumentException (Item 72) whose detail message indicates which parameters are invalid (Item 75).
為了簡潔祸泪,省略了有效性檢查吗浩。為了盡快檢測無效的參數(shù),請檢查構(gòu)建器的構(gòu)造函數(shù)和方法中的參數(shù)有效性没隘。檢查構(gòu)建方法調(diào)用的構(gòu)造函數(shù)中涉及多個參數(shù)的不變量懂扼。為了確保這些不變量不受攻擊,在從構(gòu)建器復制參數(shù)之后檢查對象字段(Item-50)右蒲。如果檢查失敗阀湿,拋出一個 IllegalArgumentException(Item-72),它的詳細消息指示哪些參數(shù)無效(Item-75)瑰妄。
The Builder pattern is well suited to class hierarchies. Use a parallel hierarchy of builders, each nested in the corresponding class. Abstract classes have abstract builders; concrete classes have concrete builders. For example,consider an abstract class at the root of a hierarchy representing various kinds of pizza:
建造者模式非常適合于類層次結(jié)構(gòu)陷嘴。使用構(gòu)建器的并行層次結(jié)構(gòu),每個構(gòu)建器都嵌套在相應(yīng)的類中间坐。抽象類有抽象類構(gòu)建器灾挨;具體類有具體類構(gòu)建器邑退。例如,考慮一個在層次結(jié)構(gòu)處于最低端的抽象類劳澄,它代表各種比薩餅:
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;
// 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
}
}
Note that Pizza.Builder is a generic type with a recursive type parameter (Item 30). This, along with the abstract self method, allows method chaining to work properly in subclasses, without the need for casts. This workaround for the fact that Java lacks a self type is known as the simulated self-type idiom. Here are two concrete subclasses of Pizza, one of which represents a standard New-York-style pizza, the other a calzone. The former has a required size parameter,while the latter lets you specify whether sauce should be inside or out:
請注意地技,Pizza.Builder 是具有遞歸類型參數(shù)的泛型類型(Item-31)。這與抽象 self 方法一起秒拔,允許方法鏈接在子類中正常工作莫矗,而不需要強制轉(zhuǎn)換。對于 Java 缺少自類型這一事實溯警,這種變通方法稱為模擬自類型習慣用法趣苏。這里有兩個具體的比薩子類狡相,一個是標準的紐約風格的比薩梯轻,另一個是 calzone。前者有一個所需的大小參數(shù)尽棕,而后者讓你指定醬料應(yīng)該是內(nèi)部還是外部:
import java.util.Objects;
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;
}
}
Note that the build method in each subclass’s builder is declared to return the correct subclass: the build method of NyPizza.Builder returns NyPizza, while the one in Calzone.Builder returns Calzone. This technique, wherein a subclass method is declared to return a subtype of the return type declared in the super-class, is known as covariant return typing. It allows clients to use these builders without the need for casting.The client code for these “hierarchical builders” is essentially identical to the code for the simple NutritionFacts builder. The example client code shown next assumes static imports on enum constants for brevity:
注意喳挑,每個子類的構(gòu)建器中的構(gòu)建方法聲明為返回正確的子類:構(gòu)建的方法 NyPizza.Builder
返回 NyPizza,而在 Calzone.Builder
則返回 Calzone滔悉。這種技術(shù)稱為協(xié)變返回類型伊诵,其中一個子類方法聲明為返回超類中聲明的返回類型的子類型。它允許客戶使用這些構(gòu)建器回官,而不需要強制轉(zhuǎn)換曹宴。這些「層次構(gòu)建器」的客戶端代碼與簡單的 NutritionFacts 構(gòu)建器的代碼基本相同。為簡潔起見歉提,下面顯示的示例客戶端代碼假定枚舉常量上的靜態(tài)導入:
NyPizza pizza = new NyPizza.Builder(SMALL)
.addTopping(SAUSAGE).addTopping(ONION).build();
Calzone calzone = new Calzone.Builder()
.addTopping(HAM).sauceInside().build();
A minor advantage of builders over constructors is that builders can have multiple varargs parameters because each parameter is specified in its own method. Alternatively, builders can aggregate the parameters passed into multiple calls to a method into a single field, as demonstrated in the addTopping method earlier.
與構(gòu)造函數(shù)相比笛坦,構(gòu)造函數(shù)的一個小優(yōu)點是構(gòu)造函數(shù)可以有多個變量參數(shù),因為每個參數(shù)都是在自己的方法中指定的苔巨“胬或者,構(gòu)建器可以將傳遞給一個方法的多個調(diào)用的參數(shù)聚合到單個字段中侄泽,如前面的 addTopping 方法中所示礁芦。
The Builder pattern is quite flexible. A single builder can be used repeatedly to build multiple objects. The parameters of the builder can be tweaked between invocations of the build method to vary the objects that are created. A builder can fill in some fields automatically upon object creation, such as a serial number that increases each time an object is created.
建造者模式非常靈活。一個構(gòu)建器可以多次用于構(gòu)建多個對象悼尾。構(gòu)建器的參數(shù)可以在構(gòu)建方法的調(diào)用之間進行調(diào)整柿扣,以改變創(chuàng)建的對象。構(gòu)建器可以在創(chuàng)建對象時自動填充某些字段闺魏,例如在每次創(chuàng)建對象時增加的序列號窄刘。
The Builder pattern has disadvantages as well. In order to create an object,you must first create its builder. While the cost of creating this builder is unlikely to be noticeable in practice, it could be a problem in 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 to make it worthwhile, say four or more. But keep in mind that you may want to add more parameters in the future. But if you start out with constructors or static factories and switch to a builder when the class evolves to the point where the number of parameters gets 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.
建造者模式也有缺點。為了創(chuàng)建一個對象舷胜,你必須首先創(chuàng)建它的構(gòu)建器娩践。雖然在實際應(yīng)用中創(chuàng)建這個構(gòu)建器的成本可能并不顯著活翩,但在以性能為關(guān)鍵的場景下,這可能會是一個問題翻伺。而且材泄,建造者模式比可伸縮構(gòu)造函數(shù)模式更冗長,因此只有在有足夠多的參數(shù)時才值得使用吨岭,比如有 4 個或更多參數(shù)時拉宗,才應(yīng)該使用它。但是請記住辣辫,你可能希望在將來添加更多的參數(shù)旦事。但是,如果你以構(gòu)造函數(shù)或靜態(tài)工廠開始急灭,直至類擴展到參數(shù)數(shù)量無法控制的程度時姐浮,也會切換到構(gòu)建器,但是過時的構(gòu)造函數(shù)或靜態(tài)工廠將很難處理葬馋。因此卖鲤,最好一開始就從構(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 many of the parameters are optional or of identical type. Client code is much easier to read and write with builders than with telescoping constructors, and builders are much safer than JavaBeans.
總之畴嘶,在設(shè)計構(gòu)造函數(shù)或靜態(tài)工廠的類時蛋逾,建造者模式是一個很好的選擇,特別是當許多參數(shù)是可選的或具有相同類型時窗悯。與可伸縮構(gòu)造函數(shù)相比区匣,使用構(gòu)建器客戶端代碼更容易讀寫,而且構(gòu)建器比 JavaBean 更安全蒋院。