静态工厂和构造器都有这样一个限制:他们当面对一个大量的操作参数都不能表现很好。思考这样一个问题,如果有一个类,它表示一包食物的营养价值的标签。这个标签有少量的必须字段——(serving size, servings per container, and calories per serving)——而这里有可能超过20个可选择的字段(——(total fat, saturated fat, trans fat, cholesterol, sodium, and so on)
这样一个类的构造器或者静态工厂方法的顺序将会是怎么样的?传统之上,程序员已经使用过重叠构造模式(telescoping constructor pattern),这种模式你可需要提供一个所有必须参数的构造器,另一个是只有一个的可选参数的构造器,第三个是两个可选参数的构造器,等等等等等....到最后在一个构造器中拥有所有的可选参数。那么这看起来是怎么样?为了简单,仅展示四个可选字段的构造器:
// 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,在只有六个参数的时候这看起来可能并不坏,但是在参数不断增加的时候,这很快就会失控。
简而言之,重叠构造模式有用,但是在有很多参数的时候他很难去写出一个客户端的代码,同时也很难去阅读它!阅读者很好奇那些参数值到底是什么意思,同时必须非常关注计算参数的数量。长期的后果将导致一邪微妙的bug。如果这个客户端意外的倒转类两个参数的位置,编译就可能不会完成(通常类型不同IDE就会检查出来,但是两个参数类型相同,IDE就无法检查,就会出现难以排查的错误)!而程序在运行时就会出现错误。
第二个替代的方案是,在面对大量的可选参数的时候,你可以使用JavaBeans模式。你可以调用一个无参的构造器来创建一个对象,然后通过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);
不幸的是,这个JavaBeans模式有一个严重的缺点,因为构造行为是通过很多次调用set方法来完成,所以每一个JavaBean可能会在构造过程中出现一个不正确的状态(a JavaBean may be in an inconsistent state partway through its construction.)这个类没有连续地实施对构造器参数的检查。计划当这个对象还是一个不一致的状态去使用这个对象,可能会导致远超过代码能控制范围的bug的错误,所以很难去调试。另一个和这个相关的缺点就是JavaBeans模式不允许去制造一个不可变对象!同时,在使用JavaBeans模式中,程序员需要去确保线程安全的问题。
通过“冻结“一个对象去降低这个短板是可行的,当一个对象的构造过程完成之前,不允许使用这个对象!但是这样的变体很笨重也很少在实际中使用。更多的情况是,由于编译器不能在一个对象使用之前确保程序员已经调用对象中的这个冻结方法,所以冻结一个对象的方式将会导致一些运行时错误。
幸运到是,这里有第三种可替代的方式,这种方式联合了 重叠构造器模式的安全性和JavaBeans模式的可读性,这就形成了建造者模式(Builder pattern)。在使用建造者模式,我们不是首先就来创建一个想要的对象!首先客户端调用一个有所有必须参数的构造器(或者静态工厂方法)来得到一个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方法返回一个建造者以至于能够使用一个流畅的链式调用。这里可以看一看在客户端的代码是怎么样:
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).carbohydrate(27).build();
这个客户端的代码很容易去写,更重要的是,它很容易去阅读。这个建造者模式的可选参数可以在Python和Scala语言中直接发现(比如python中的关键参数和参数有默认值的特性可以就是建造者模式的体现)。
为了简洁,在这里有合理的检查被省略了。为了尽可能的检查验证参数,检查参数应该出现在在builder的构造器和方法中!在构造器调用build方法的时候也要检查builder的不可变性(类似集合的modCount变量??)。为了确保风险的不可改变,在从builder上复制字段到对象上时必须做出做这个检查,如果检查失败,就要抛出IllegalArgumentException异常!异常的细节即是这个参数没有通过验证的信息!
建造者模式非常适合类层次结构!(The Builder pattern is well suited to class hierarchies.),在每一个内嵌在对应的class,使用一个建造者的平行层次。抽象类有抽象的建造者,具体的类有具体的建造者!例如,思考,一个在根节点层次的抽象类将会有各种各样的表现!
// Builder pattern for class hierarchies
public abstract class Pizza {
public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE } final Set toppings;
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),这个和抽象自身的方法一起,允许方法链式的在没有额外的花费就能良好的在子类运行。
这个技术工作的的原因,事实上是由于Java缺少一个它自己已知的包装的语言特性。(This workaround for the fact that Java lacks a self type is known as the simulated self-type idiom.)
这里有两个具体的Pizza的子类,一个提供一个基础的New-York-style pizza,另一个,构成需要一个size的参数,这个参数(size)将会导致后者对象的调味汁应该放在外面还是里面。
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;
}
}
值得注意的是,这个build方法在每个子类的builder被声明将要返回正确的子类:NyPizza.Builder的build方法返回NyPizza,当一个在Calzone.Builder将返回一个Calzone。这个在子类中声明具体的返回类型,但是这个方法的顶级声明在父类中出现的技术被称作covariant return typing。它运行客户端不需要多余耗费地使用这些builider。
对于这个层次builder,客户端的代码本质上和NutritionFacts 的builder的代码是相同的。这个接下来的客户端的代码展示短暂地从枚举常量中静态导入的例程。
NyPizza pizza = new NyPizza.Builder(SMALL).addTopping(SAUSAGE).addTopping(ONION).build();
Calzone calzone = new Calzone.Builder().addTopping(HAM).sauceInside().build();
建造者模式在构造器上的一个主要的优点是builder可以有多重的可变化的参数,因为每一个参数被指定在他们独有的方法中。要不然,builder可以通过调用方法将这些参数合并到一个单一的字段,如在addTopping之前的builder构造方法。
建造者模式是非常灵活的!一个简单的建造者可以重复使用地构建多个对象,builder的参数可以在build方法调用之前做合理的调整来改变他们将要创建的对象。一个建造者可以像创建的对象自动地填充一些字段。比如说某一个字段,是在每一次创建一个对象之后会连续增长的数字。
建造者模式也有他的缺点,为了创建一个对象,你必须首先创建一个builder,然而在实际中,创建builder的花费不太可能被关注到。这可能会是一些问题在性能临界位置(it could be a problem in performance-critical situations.)。同时,建造者模式比重叠构造模式更加冗余,所以它可能只有在参数较多的情况下才值得被使用,一般而言,大于等于四个可选的参数。但是,总是要记住你未来可能会添加新的参数,而如果你开始时使用构造器过着静态工厂来创建对象,当类演化到一个参数的数量出现失控时才改变成一个builder,这些废弃的构造器或者静态工厂方法将让人非常难受。因此,通常而言最好首先就使用建造者作为开始。
总结:建造者模式是在设计一个构造参数可能会失控的类的一个很好的选择。尤其是在很多参数是可选的或者是相同的类型,客户端代码会非常简单易读当使用建造者模式而不是重叠构造器,同时建造者模式也比JavaBeans模式更加安全