天天看点

Java架构师教你写代码(二) - 使用建造者替代多参数的构造器(上)1 伸缩式构造器模式2 JavaBean 模式3 建造者模式

静态工厂和构造器的局限:对于大量可选参数情况,难以做到很好的扩展。

比如一个类,表示包装食品上的营养标签。

有些字段是必需的:净含量、毛重和每单位份量的卡路里,

还有 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 实例

Java架构师教你写代码(二) - 使用建造者替代多参数的构造器(上)1 伸缩式构造器模式2 JavaBean 模式3 建造者模式

It is easy, if a bit wordy(adj.冗长的), to create instances, and easy to read the resulting(v.产生;adj.作为结果的) code:

2.2 优点

该模式没有可伸缩构造函数模式的缺点。创建实例很容易,虽有点冗长,但可读性较好。

Java架构师教你写代码(二) - 使用建造者替代多参数的构造器(上)1 伸缩式构造器模式2 JavaBean 模式3 建造者模式

2.3 缺点

  • 因为构造过程被拆成多个set调用,所以 JavaBean 在并发下构造过程可能处于不一致。无法仅通过校验构造器参数的有效性来保证一致性。在不一致的状态下尝试使用对象可能会导致错误的发生,这比包含bug的代码还难调试。

JavaBean 模式还泯灭了使类不可变的可能性,且需SE费心思确保线程安全。

通过在对象构造完成时手动「冻结」对象,并在冻结之前不允许使用对象,可以减少这些缺陷,但是这种变通方式很笨拙,在实践中很少使用。此外,它可能在运行时导致错误,因为编译器不能确保程序员在使用对象之前调用它的 freeze 方法。

幸好,还有第三种方案,它结合可伸缩构造器模式的安全性和 JavaBean 模式的可读性

3 建造者模式

  1. 不直接生成所需对象,而使用所有必需参数调用构造器(或静态工厂),获得一个 builder 对象
  2. 然后客户端在构建器对象上调用 setter 方法设置每个感兴趣的可选参数
  3. 最后调用一个无参build方法来生成对象,这通常是不可变的。builder通常是它构建的类的静态成员类。

3.1 实例

Java架构师教你写代码(二) - 使用建造者替代多参数的构造器(上)1 伸缩式构造器模式2 JavaBean 模式3 建造者模式

NutritionFacts 类不可变,所有默认参数值都在一个位置。builder的 setter 方法返回builder本身,便于链式调用,得到流式 API。形如下:

Java架构师教你写代码(二) - 使用建造者替代多参数的构造器(上)1 伸缩式构造器模式2 JavaBean 模式3 建造者模式

特点

这样的代码易于编写,可读性佳。

为简洁,省略有效性检查。为尽快检测到无效参数,可在builder的构造器和方法中校验参数有效性。检查不可变量,包括build方法调用的构造器中的多个参数。为确保这些不可变量免受攻击,从builder复制参数后检查对象字段。如果检查失败,抛 IllegalArgumentException,指示哪些参数无效。