静态工厂和构造器的局限:对于大量可选参数情况,难以做到很好的扩展。
比如一个类,表示包装食品上的营养标签。
有些字段是必需的:净含量、毛重和每单位份量的卡路里,
还有 20 个可选字段,如:总脂肪、饱和脂肪、反式脂肪、胆固醇、钠…
大多食品只使用可选字段中的少数,且非零值。
这样的类怎么编写构造器或静态工厂?
SE 通常使用可伸缩构造器模式:只向构造函数提供必需的参数。
提供的第一个构造器只有必需参数,第二个构造器有一个可选参数…以此类推,最后一个构造函数具有所有可选参数。
1 伸缩式构造器模式
// 伸缩式构造器模式 - 伸缩性差 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。
如果调用不小心颠倒俩参数,编译器不报错,但程序在运行时会出错。
对于许多可选构造器参数,另一可行方案是
2 JavaBean 模式
调用无参构造器创建对象,然后调用 setter 方法设置所需参数和感兴趣的可选参数。
2.1 实例
It is easy, if a bit wordy(adj.冗长的), to create instances, and easy to read the resulting(v.产生;adj.作为结果的) code:
2.2 优点
该模式没有可伸缩构造函数模式的缺点。创建实例很容易,虽有点冗长,但可读性较好。
2.3 缺点
- 因为构造过程被拆成多个set调用,所以 JavaBean 在并发下构造过程可能处于不一致。无法仅通过校验构造器参数的有效性来保证一致性。在不一致的状态下尝试使用对象可能会导致错误的发生,这比包含bug的代码还难调试。
JavaBean 模式还泯灭了使类不可变的可能性,且需SE费心思确保线程安全。
通过在对象构造完成时手动「冻结」对象,并在冻结之前不允许使用对象,可以减少这些缺陷,但是这种变通方式很笨拙,在实践中很少使用。此外,它可能在运行时导致错误,因为编译器不能确保程序员在使用对象之前调用它的 freeze 方法。
幸好,还有第三种方案,它结合可伸缩构造器模式的安全性和 JavaBean 模式的可读性
3 建造者模式
- 不直接生成所需对象,而使用所有必需参数调用构造器(或静态工厂),获得一个 builder 对象
- 然后客户端在构建器对象上调用 setter 方法设置每个感兴趣的可选参数
- 最后调用一个无参build方法来生成对象,这通常是不可变的。builder通常是它构建的类的静态成员类。
3.1 实例
NutritionFacts 类不可变,所有默认参数值都在一个位置。builder的 setter 方法返回builder本身,便于链式调用,得到流式 API。形如下:
特点
这样的代码易于编写,可读性佳。
为简洁,省略有效性检查。为尽快检测到无效参数,可在builder的构造器和方法中校验参数有效性。检查不可变量,包括build方法调用的构造器中的多个参数。为确保这些不可变量免受攻击,从builder复制参数后检查对象字段。如果检查失败,抛 IllegalArgumentException,指示哪些参数无效。