ITEM 2: CONSIDER A BUILDER WHEN FACED WITH MANY CONSTRUCTOR PARAMETERS
静态工厂方法和构造函数都不能解决一个问题—— 当成员变量很多时,它们没办法控制入参。例如一个标示食品营养标签的类,它包含很多成员:每餐的份量,每餐的分量,每餐的卡路里——以及20多个可选领域——总脂肪,饱和脂肪,反式脂肪,胆固醇,钠等等。大多数变量只有某些可选的几个具有非零值。
我们应该怎样编写构造函数或静态工厂? 传统的方法是,程序员使用
可伸缩构造函数模式,在该模式中,只提供一个具有所需参数的构造函数,另一个具有一个可选参数,第三个具有两个可选参数,依此类推,最后提供一个具有所有可选参数的构造函数。这是它在实践中的样子。为了简洁起见,只显示了四个可选字段:
public class NutritionFacts {
private final int servingSize; // (mL) required private final int servings; // (per container) required
private final int calories;
private final int fat;
private final int sodium;
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
this.fat
this.sodium
this.carbohydrate = carbohydrate;
}
}
当我们想创建一个实例,我们可以选择那个包含我们需要的所有参数的、入参列表最短的那个构造函数。
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
可能有些参数我们并不想set,但我们别无选择。在上面这个例子里,我们将'fat' 设置为0。6个参数看起来也许并不太糟,但当数量增加,程序就开始失控了。简单来说,虽然伸缩构造函数模式确实可以工作,但这些构造函数对用户非常不友好,难以理解和使用。用户需要仔细阅读函数签名/文档才能知道这些参数的意义。具有相同类型的长参数可能导致细微的bug,如果用户不小心将参数填错位置,编译器并不会报警,这个错误将一直隐藏直到运行时暴露。
还有一种解决方案,当存在许多可选参数时,我们可以选择JavaBeans模式,调用一个无参数构造函数来创建对象,然后调用setter方法来设置每个必需参数和每个感兴趣的可选参数:
public class NutritionFacts {
// Parameters initialized to default values (if any)
private int servingSize = -1; // Required; no default value
private int servings = -1;
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);
不幸的是,JavaBean也有缺点:由于构造是分割在不同的函数调用中,所以在构造过程中实例可能处于不一致状态,如果试图在实例处于不一致的状态下使用,可能会导致一些奇怪的问题,并且很难找出原因。另一个麻烦是,JavaBean模式无法使用在不可变类上,并且需要程序员负责保证线程安全。可以在实例未完成前“冻结”它,并且“冻结”时不允许使用,但这种方式比较笨重,很少使用。此外,它可能在运行时导致错误,因为编译器不能确保程序员在使用对象之前调用冻结方法。
幸运的是,还有第三种方法可以将伸缩构造函数模式的安全性与JavaBean模式的可读性结合起来。它是构建器模式的一种形式。
用户调用Builder类的构造函数,先构造必须参数,再调用类setter方法填入可选参数,最后用户调用build()方法生成实例,构建器通常是它构建的类的静态成员类。下面是它在实践中的样子:
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 类是不可变的,并且它所有的必填参数都放在一起初始化,构建器的setter方法返回构建器本身,以便可以链接调用,从而生成一个连贯的API。下面是客户端代码的样子:
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).carbohydrate(27).build();
以上的代码很容易读写。Builder模式模拟了Python和Scala中的命名可选参数。这里省略了有效性检查,为了尽快发现无效参数,可以在builder的构造器和方法里添加check。在build方法调用的构造方法中,检查包含多个参数的不变性。要确保这些不变量不受攻击,请在从builder复制参数后检查对象字段。如果检查失败,抛出 IllegalArgumentException 异常指示哪些参数无效。
构建器模式非常适合于类层次结构。每一层类有对应的builder,Abstract classes 有 abstract builders; concrete classes 有 concrete builders。例如,考虑一个类层次结构,最上层是一个Abstract class :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();
// 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缺乏自类型这一事实,这种解决方法称为模拟自类型习语。
现在这里有两个Pizza的子类,一个是 New-York-style pizza, 另一个是 calzone(半圆形烤乳酪馅饼?),前者有一个必要的大小参数,而后者可以让你指定酱汁应该在里面还是外面:
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 pizza = new NyPizza.Builder(SMALL).addTopping(SAUSAGE).addTopping(ONION).build();
Calzone calzone = new Calzone.Builder() .addTopping(HAM).sauceInside().build();
Builder相对于Constructor的一个微小优势在于,builder可以有多个可变参数,每个参数在自己的方法里指定。或者,构建器可以将传递给方法的多个调用的参数聚合到单个字段中,如上例中的addTopping()。
Builder模式非常灵活,一个builder可以被重复使用,构建多个的实例。参数可以在构建方法的调用之间进行调整,以更改创建的对象。构建器可以在对象创建时自动填充一些字段,比如序列号,每次创建对象时序列号都会增加。
Builder模式也有缺点:为了创建实例,我们必须先创建它的builder。虽然在实践中创建builder的成本不太可能高,但在比较关注性能的情况下,它可能会成为一个问题。
Builder模式也比 constructor 更冗长,所以最好在有足够多参数的情况下使用,确保我们获得的收益足以覆盖付出的代价。但是我们可能希望在将来添加更多的参数,而如果我们从构造函数或静态工厂方法开始,并在参数数量无法控制的时候切换到构建器,那么过时的构造函数或静态工厂就会像一个疼痛的拇指一样突出。因此,最好从构建器开始。
总之,在为有多个参数的类设计构造函数或静态工厂时(尤其是当许多参数是可选的或具有相同类型时),Builder模式是一个很好的选择。使用builder比使用可伸缩构造器更容易读写客户机代码,而且构建器比Javabean更安全。
网友评论