Java架构师教你写代码(二) - 使用建造者替代多参数的构造器

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

比如一个类,表示包装食品上的营养标签。
有些字段是必需的:净含量、毛重和每单位份量的卡路里,
还有 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张图片
It is easy, if a bit wordy(adj.冗长的), to create instances, and easy to read the resulting(v.产生;adj.作为结果的) code:

2.2 优点

该模式没有可伸缩构造函数模式的缺点。创建实例很容易,虽有点冗长,但可读性较好。
Java架构师教你写代码(二) - 使用建造者替代多参数的构造器_第2张图片

2.3 缺点

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

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

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

3 建造者模式

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

3.1 实例

Java架构师教你写代码(二) - 使用建造者替代多参数的构造器_第3张图片

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

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

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

4 建造者模式适于类层次结构

使用构建器的平行层次结构,每个构建器都嵌套在相应类中。
抽象类有抽象类构建器;具体类有具体类构建器。

4.1 实例

类继承结构中处于最底端的抽象类:各种比萨:
Java架构师教你写代码(二) - 使用建造者替代多参数的构造器_第4张图片

BasePizza.Builder 泛型类型,有个递归类型的参数。和抽象的 self 方法一起,允许在子类中适当地进行方法链接,而无需强制转换。对于 Java 缺少自类型这一事实,这种变通方法是模拟自类型习惯用法。
有两个具体的比萨子类

  1. 标准的纽约风格的比萨
  2. calzone
    Java架构师教你写代码(二) - 使用建造者替代多参数的构造器_第5张图片
    Java架构师教你写代码(二) - 使用建造者替代多参数的构造器_第6张图片

每个子类的构建器中的build方法声明为返回正确的子类:

  • NyPizza.Builder 返回 NyPizza
  • Calzone.Builder 返回 Calzone

子类方法声明为返回父类中声明的返回类型的子类型(协变返回类型)。通过构建器,无需类型转换。
Java架构师教你写代码(二) - 使用建造者替代多参数的构造器_第7张图片

与构造器比,优势是可以有多个可变参数,因为每个参数都是在自己的方法中指定的。
构建器可以将多次调用某一方法而传入的参数聚合到一个字段
Java架构师教你写代码(二) - 使用建造者替代多参数的构造器_第8张图片

5 优点

建造者模式灵活,一个构建器可被重复使用而构建多个对象。
构建器参数可以在调用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.

6 缺点

  1. 为创建对象,须先创建构建器。虽然在实践中创建构建器成本可能不显著,但在性能场景,可能是问题
  2. 建造者模式比可伸缩构造器模式更冗长,只在有足够多参数时值得,≥4个时使用吧
  3. 你可能在将来添加更多参数。但是,如果以构造器或静态工厂开始,直至类扩展到参数失控时,也会切换到构建器,但是过时的构造器或静态工厂很难处理。因此,最好一开始就从构建器开始

7 总结

在设计构造器或静态工厂的类时,有许多参数是可选的或具有相同类型时,建造者模式是很好的选择。
与可伸缩构造器比,使用构建器客户端代码更容易读写,而且比 JavaBean 安全。

翻译并整理自 effective java 第三版英文版

你可能感兴趣的:(#,Effective,Java)