1. 问题引入
在Java中,会碰到这种情形:以一个类为例,它表示包装食品上的营养标签。这个标签包含必需字段,如:净含量、毛重和每单位份量的卡路里,可选的字段,如:总脂肪、饱和脂肪、反式脂肪、胆固醇、钠……。
如何对这种有大量可选参数的构造函数进行更好的扩展呢?
2. 问题解析
2.1 可伸缩构造函数
传统的处理方式是使用可伸缩构造函数,在这种模式中,只向构造函数提供必需的参数。即,向第一个构造函数提供单个可选参数,向第二个构造函数提供两个可选参数,以此类推,最后一个构造函数是具有所有可选参数的。
类似下面的代码(篇幅原因,只展示4个可选字段的情况):
// 伸缩构造模式-不能很好的扩展
public class NutritionFacts {
private final int servingSize; // (mL) 必须字段
private final int servings; // (per container) 必须字段
private final int calories; // (per serving) 可选
private final int fat; // (g/serving) 可选
private final int sodium; // (mg/serving) 可选
private final int carbohydrate; // (g/serving) 可选
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;
这种方式虽然可行,但是当有很多参数时,编写客户端代码是很困难的,而且读起来更困难。阅读代码的人想知道所有这些值是什么意思,必须仔细清点参数。相同类型参数的长序列会导致细微的错误。如果客户端不小心倒转了两个这样的参数,编译器不会报错,但是程序会在运行时出错。
2.2 JavaBean模式
这种方式,通过调用无参构造来创建对象,然后调用setter 方法来设置每个所需的参数。
// JavaBeans模式 - 容易导致程序构建的不一致性,让类变成可变的
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; }
}
JavaBean让创建实例很容易,虽然有点冗长,单代码容易阅读,如下:
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
不幸的是,JavaBean 模式本身有严重的缺点。因为构建是在多个调用之间进行的,所以 JavaBean 可能在构建的过程中处于不一致的状态。该类不能仅通过检查构造函数参数的有效性来强制一致性。在不一致的状态下尝试使用对象可能会导致错误的发生,而包含这些错误的代码很难调试。一个相关的缺点是,JavaBean 模式排除了使类不可变的可能性,并且需要程序员额外的努力来确保线程安全。
2.3 构建器
第三种选择-构建器,它结合了可伸缩构造函数模式的安全性和 JavaBean模式的可读性。
它是建造者模式的一种形式。客户端不直接生成所需的对象,而是使用所有必需的参数调用构造函数(或静态工厂),并获得一个 builder对象。然后,客户端在构建器对象上调用像setter这样的方法来设置每个感兴趣的可选参数。最后,客户端调用一个无参数的构建方法来生成对象,这通常是不可变的。
构建器通常是它构建的类的静态成员类。
使用示例:
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(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;
}
public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();
}
}
NutritionFacts 类是不可变的,所有参数默认值都在一个位置。构建器的setter方法返回构建器本身,这样就可以链式调用,从而得到一个流畅的 API。
客户端使用:
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();
参考:
Item 2: Consider a builder when faced with many constructor parameters(当构造函数有多个参数时,考虑改用构建器);