静态工厂和构造器的局限:对于大量可选参数情况,难以做到很好的扩展。
比如一个类,表示包装食品上的营养标签。
有些字段是必需的:净含量、毛重和每单位份量的卡路里,
还有 20 个可选字段,如:总脂肪、饱和脂肪、反式脂肪、胆固醇、钠…
大多食品只使用可选字段中的少数,且非零值。
// 伸缩式构造器模式 - 伸缩性差
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;
}
}
想创建一个实例,就使用参数列表最短构造器:
NutritionFacts cocaCola =new NutritionFacts(240, 8, 100, 0, 35, 27);
该构造器包含许多额外参数,而且还必须得给它们传递值。
本例中,为 fat 传递了一个0。只有六个参数时,这可能看起来不拉几,但随着参数增加,很快失控。
可伸缩构造器模式可以用,但当有很多参数时,客户端代码很难写,可读性也差 。
阅读者想知道这些值啥意思,必须清点参数。而长序列的相同类型参数也极易导致bug。
如果调用不小心颠倒俩参数,编译器不报错,但程序在运行时会出错。
对于许多可选构造器参数,另一可行方案是
调用无参构造器创建对象,然后调用 setter 方法设置所需参数和感兴趣的可选参数。
It is easy, if a bit wordy(adj.冗长的), to create instances, and easy to read the resulting(v.产生;adj.作为结果的) code:
该模式没有可伸缩构造函数模式的缺点。创建实例很容易,虽有点冗长,但可读性较好。
通过在对象构造完成时手动「冻结」对象,并在冻结之前不允许使用对象,可以减少这些缺陷,但是这种变通方式很笨拙,在实践中很少使用。此外,它可能在运行时导致错误,因为编译器不能确保程序员在使用对象之前调用它的 freeze 方法。
幸好,还有第三种方案,它结合可伸缩构造器模式的安全性和 JavaBean 模式的可读性
NutritionFacts 类不可变,所有默认参数值都在一个位置。builder的 setter 方法返回builder本身,便于链式调用,得到流式 API。形如下:
为简洁,省略有效性检查。为尽快检测到无效参数,可在builder的构造器和方法中校验参数有效性。检查不可变量,包括build方法调用的构造器中的多个参数。为确保这些不可变量免受攻击,从builder复制参数后检查对象字段。如果检查失败,抛 IllegalArgumentException,指示哪些参数无效。
使用构建器的平行层次结构,每个构建器都嵌套在相应类中。
抽象类有抽象类构建器;具体类有具体类构建器。
BasePizza.Builder
泛型类型,有个递归类型的参数。和抽象的 self 方法一起,允许在子类中适当地进行方法链接,而无需强制转换。对于 Java 缺少自类型这一事实,这种变通方法是模拟自类型习惯用法。
有两个具体的比萨子类
每个子类的构建器中的build方法声明为返回正确的子类:
NyPizza.Builder
返回 NyPizzaCalzone.Builder
返回 Calzone子类方法声明为返回父类中声明的返回类型的子类型(协变返回类型)。通过构建器,无需类型转换。
与构造器比,优势是可以有多个可变参数,因为每个参数都是在自己的方法中指定的。
构建器可以将多次调用某一方法而传入的参数聚合到一个字段
建造者模式灵活,一个构建器可被重复使用而构建多个对象。
构建器参数可以在调用build方法创建对象间调整,也可随着不同的对象而改变。
构建器可自动填充某些字段,例如在每次创建对象时自动增加序列号。
Also, the Builder pattern is more verbose than the telescoping constructor pattern, so it should be used only if there are enough parameters to make it worthwhile, say four or more. But keep in mind that you may want to add more parameters in the future. But if you start out with constructors or static factories and switch to a builder when the class evolves to the point where the number of parameters gets out of hand, the obsolete constructors or static factories will stick out like a sore thumb. Therefore, it’s often better to start with a builder in the first place.
在设计构造器或静态工厂的类时,有许多参数是可选的或具有相同类型时,建造者模式是很好的选择。
与可伸缩构造器比,使用构建器客户端代码更容易读写,而且比 JavaBean 安全。
翻译并整理自 effective java 第三版英文版