《Effective Java 第三版》笔记之二 当构造参数很多的时候考虑使用builder | DataLearnerAI静态工厂和构造方法都有一个缺点:当有很多可选参数的时候,其扩展性并不是很好。例如,考虑这样一个类,它表示食物包装上的营养物质标签。这些标签有一部分是必须的字段——例如分量大小、每个包装容器包含的分量大小、每份物质包含的卡路里等,还有一部分是可选字段——例如总的脂肪含量、饱和脂肪含量、反式脂肪含量等等。大多数食品只有一小部分字段是非零的结果。
对于这样一个类,要如何使用构造方法或者是静态工厂方法呢?传统上,编程者可以使用重叠构造函数模式(telescoping constructor pattern),即在某个构造方法中只包含必须的字段,然后添加其他的构造方法包含其他可选字段。举个例子:假设只有4个可选字段:
Follow DataLearner WeChat for the latest AI updates
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 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方法将所需的字段赋值,例如:
public class NutritionFacts {
private int servingSize = -1;
private int servings = -1;
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public NutritionFacts() { }
public void setServingSize(int val) { servingSize = val; }
public void setServings(int val) { servings = val; }
public void setCalories( val) { calories = val; }
{ fat = val; }
{ sodium = 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通常都是一个静态的成员类:
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 {
private final int servingSize;
private final int servings;
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public Builder {
.servingSize = servingSize;
.servings = servings;
}
Builder {
calories = val;
;
}
Builder {
fat = val;
;
}
Builder {
sodium = val;
;
}
Builder {
carbohydrate = val;
;
}
NutritionFacts {
();
}
}
{
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:
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();
protected abstract T self();
}
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone();
}
}
注意,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 .Builder<Builder> {
;
Builder {
sauceInside = ;
;
}
Calzone {
();
}
Builder { ; }
}
{
(builder);
sauceInside = builder.sauceInside;
}
}
注意到,子类的builder被声明为返回正确的类型了。NyPizza.Builder的build方法返回的是NyPizza类,而Calzone.Builder返回的是Calzone类。这种子类方法返回父类返回值的子类型称之为协变返回类型(covariant return typing)。它允许子类可以直接使用这些builders而不需要做强制转化。
int
public
void
setFat
(int val)
public
void
setSodium
(int val)
public
void
setCarbohydrate
(int val)
(int servingSize, int servings)
this
this
public
calories
(int val)
return
this
public
fat
(int val)
return
this
public
sodium
(int val)
return
this
public
carbohydrate
(int val)
return
this
public
build
()
return
new
NutritionFacts
this
private
NutritionFacts
(Builder builder)
extends
Pizza
private
boolean
sauceInside
=
false
public
sauceInside
()
true
return
this
@Override
public
build
()
return
new
Calzone
this
@Override
protected
self
()
return
this
private
Calzone
(Builder builder)
super