《Effective Java 第三版》笔记之二 当构造参数很多的时候考虑使用builder
静态工厂和构造方法都有一个缺点:当有很多可选参数的时候,其扩展性并不是很好。例如,考虑这样一个类,它表示食物包装上的营养物质标签。这些标签有一部分是必须的字段——例如分量大小、每个包装容器包含的分量大小、每份物质包含的卡路里等,还有一部分是可选字段——例如总的脂肪含量、饱和脂肪含量、反式脂肪含量等等。大多数食品只有一小部分字段是非零的结果。
对于这样一个类,要如何使用构造方法或者是静态工厂方法呢?传统上,编程者可以使用重叠构造函数模式(telescoping constructor pattern),即在某个构造方法中只包含必须的字段,然后添加其他的构造方法包含其他可选字段。举个例子:假设只有4个可选字段:
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的模式来解决这个问题,你可以调用一个无参数的构造函数来创建对象,然后使用set方法将所需的字段赋值,例如:
// 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);
然而,JaveBeans本身有很大的缺点。由于构造过程有多次不同的调用,因此JavaBeans可能会产生不一致的情况。例如,JavaBeans类不能只通过检查构造函数参数的有效性来保证一致性。当一个对象处于一种不一致的状态时,试图使用它可能会引起失败,这个失败很难从包含错误的代码中去掉,因此很难调试。与此相关的一个缺点是JavaBeans的模式无法创建不可变的类,因此需要编程者花费其他成本来保证线程安全。
当构造工作完成时,可以通过手动『冰冻』对象并且在冰冻完成之前不允许使用它来弥补这个缺点,但这种方式太笨重了,在实践中很少使用。而且,由于编译器不能保证程序员在使用对象之前调用了冰冻方法,因此它可能在运行时引起错误。
幸运的是,有第三种方法既保证有重叠构造函数模式的安全性,也有JavaBeans的简洁性。这就是生成器模式(Builder pattern)。客户端使用构造方法来初始化所有必要的字段,然后使用类似setter方法来构建可选参数。最终,客户端使用一个无参的builder方法来产生一个对象,通常该对象都是不可变的。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就是不可变类,所有的默认参数都在一个地方。builder的setter方法返回builder本身从而使得可以链式调用API。调用的方法如下:
NutritionFacts cocaCola = new NutritionFacts.Builder(240,8).calories(100).sodium(35).carbohydrate(27).build();
这种API很容易写,且阅读起来也很方便。生成器模式模拟了Python和Scala中命名可选参数。
为了简短起见,参数的有效性检验在这里没有写出来。为了尽快的检测到无效的参数,可以在builder的构造器和方法中检验。检查有build方法调用的构造方法涉及到多个参数的不可变量。为了防止这些不可变量收到攻击,从builder中复制参数后对对象字段进行检验。如果检测失败,抛出IllegalArgumentException异常,可以显示哪些参数是无效的。
生成器模式非常适合具有层次结构的类。使用并行的层次构造器,每一个都被嵌套在相关的类中。抽象类有抽象的builder; 具体的类有具体的builder。例如,考虑一个层次类的根节点是一个抽象类,代表了不同的pizza:
// 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
}
}
注意,Pizza.Builder是一个有着递归参数的通用类型(泛型)。它和抽象的self方法一起,允许子类中的方法进行链式调用,而不需要转换。这个方法实际上是Java确实self类型的一个变通解决方案,这个类型通常称为模拟自我类型( the simulated self-type idiom)。
现在有两个具体的Pizza子类,一个代表了标准的纽约式pizza,一个是意式包馅比萨(calzone)。前者需要大小(size)这个参数,后者需要指定酱要放在里面还是外面。
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;
}
}
注意到,子类的builder被声明为返回正确的类型了。NyPizza.Builder的build方法返回的是NyPizza类,而Calzone.Builder返回的是Calzone类。这种子类方法返回父类返回值的子类型称之为协变返回类型(covariant return typing)。它允许子类可以直接使用这些builders而不需要做强制转化。
欢迎大家关注DataLearner官方微信,接受最新的AI技术推送
