Effective Java(3rd)-Item2:当构造方法有很多参数的时候考虑使用建造者模式

  静态工厂和构造器都有一个限制:他们对于大量的可选参数不能实现很好的扩展。考虑
一个类代表出现在包装食品上营养成分标签的情况。这些标签有一些必填项——分量,每份容器的分量,每份卡路里量以及二十多类可选参数,比如总脂肪,饱和脂肪,反式脂肪,胆固醇,钠等。大多数产品只有少数这些可选字段具有非零值.
  这样的一个类你如何通过构造方法或者静态工厂方法来表示呢?传统上,程序员会使用重叠构造模式(telescoping constructor),也就是提供一个只有必填参数的构造方法,再提供一个有着一个可选参数的构造方法,再提供一个有两个可选参数的构造方法,以此类推。最终提供一个有着所有可选参数的构造方法。下面是以上描述的例子.简单起见,只展示了四个可选区域。

Effective Java(3rd)-Item2:当构造方法有很多参数的时候考虑使用建造者模式_第1张图片
image.png

Effective Java(3rd)-Item2:当构造方法有很多参数的时候考虑使用建造者模式_第2张图片
image.png

  如果要创建实例,可以使用带有包含要设置的所有参数的最短参数列表的构造函数:

NutritionFacts cocaCola =
        new NutritionFacts(240, 8, 100, 0, 35, 27);

  通常这个构造器调用将需要调用许多不想设置的参数,但是无论如何你还是要被迫传递这些参数。在这种情况,我们为fat这个属性传递值0。只有六个参数的时候可能没有那么糟糕,但是当参数的数量增加后就会变得一发不可收拾。
  简而言之,重叠构造器模式是有效的,但是很难用它去实现有很多参数的情况,同时,这样也很难去阅读。读者会疑惑这些所有值都是什么意思,也会更加专注数参数的数量。一长串相同类型的参数会造成微妙的bug。如果客户端偶然反向了两个相同类型的参数,编译器不会报错,但是程序将在运行时发生错误行为。
  当你面对一个构造器有许多可选参数时,你可以选择使用JavaBean模式。你可以调用一个无参方法来创建这个对象,然后将每个需要的参数set进去:

Effective Java(3rd)-Item2:当构造方法有很多参数的时候考虑使用建造者模式_第3张图片
image.png

Effective Java(3rd)-Item2:当构造方法有很多参数的时候考虑使用建造者模式_第4张图片
image.png

  这个模式就没有重叠构造器模式的缺点。它创建实例很简单,就是有点啰嗦,但是读代码也很容易。


Effective Java(3rd)-Item2:当构造方法有很多参数的时候考虑使用建造者模式_第5张图片
image.png

  不幸的是,JavaBean模式本身有着严重的缺点。因为构造跨越多次调用,JavaBean可能在其构造的中途处于不一致的状态。仅仅检查构造器参数的有效性,该类并没有强制一致性的选项。尝试当这个对象在一个不一致的状态下使用该对象,可能会导致与包含该bug的错误代码相距甚远的失败,从而难以调试。相关缺点是JavaBean模式排除了使类不可变的可能性,需要程序员额外的努力来保证线程安全。
  当构造方法完成时通过手动“凝固”对象来阻止被使用直到冻住,这是可行的,但是这个变体是笨重的,并且在实践中几乎不被使用。此外,这会在运行时导致错误,因为编译器不能确保程序员在使用对象前调用冻结方法。

  幸运的是,这里有第三个替代方案结合了重叠构造器模式的安全性和JavaBean模式的可读性。这就是建造者模式的其中一种形式。不直接创建期望的对象,而是客户端调用一个带有所有需要参数的构造方法(或静态工厂)来得到一个建造器对象(builder object)。然后客户端调用类似setter的方法对这个建造器对象设置每个选择的参数。最后,客户端调用无参方法build来构成对象,这通常是不可变的。建造器在build时通常是一个静态成员类(item17),如下描述了在实践中是如何表现的:

Effective Java(3rd)-Item2:当构造方法有很多参数的时候考虑使用建造者模式_第6张图片
image.png

  NutritionFacts类是不可变的(译者注:所谓的不可变类是指这个类的实例一旦创建完成后,就不能改变其成员变量值)。所有参数的默认值在同一地方。建造器的setter方法返回了这个建造器本身以至于可以进行链式调用,生成了一个流畅的api(fluent API),如下描述客户端的代码:

image.png

这样的客户端代码容易书写,更重要的是,更容易去阅读。建造者模式模拟在Python和Scala中找到命名可选参数
  有效性校验为了简短而忽略了。为了尽快检测无效参数,在建造器和方法中来进行参数有效性的检查。在构造方法被build方法调用时检查不变量涉及多参数。为了确保这些不变量免受攻击,在从建造器复制参数后对对象字段进行检查 (item50)。如果检查失败,抛出一个IllegalArgumentException(item72)详细描述消息来表明哪一个参数是无效的( item75)。
  建造者模式非常适合类层次结构。使用并行的建造器层次结构,每个建造器嵌套在相应的类中。抽象类有抽象建造器;具体类有具体的建造器。举个例子,考虑一个抽象类代表不同种类的披萨的父类:

Effective Java(3rd)-Item2:当构造方法有很多参数的时候考虑使用建造者模式_第7张图片
image.png

  注意到Pizza.builder是一个带着递归类型参数的泛型类型(generic type)(item30),这样,除了抽象类本身的方法以外,也允许方法链在子类中正常工作,而没有必要强制类型转换。如此解决方法是基于Java缺少一个自我类型从而模拟自我类型的习惯。
  这里有两个Pizza的实现类,其中一个代表了标准纽约风格披萨,另一个是卡颂。前者需要一个大小的参数,后者需要你确定酱料是放进去还是分开:

Effective Java(3rd)-Item2:当构造方法有很多参数的时候考虑使用建造者模式_第8张图片
image.png

  注意到build方法在不同子类的建造器中被声明返回相应的子类:NyPizza的build方法返回NyPizza,另一个就返回CalZone。这种技巧,其中声明子类方法返回父类中声明的返回类型的子类型,称为协变式返回类型。这允许客户端使用这些建造器无需强制类型转换。
  这些“有层次的建造器”的客户端代码基本上与简单NutritionFacts建造器相同。为了简洁起见,如下示例的客户端代码假定静态引入了枚举常量:
Effective Java(3rd)-Item2:当构造方法有很多参数的时候考虑使用建造者模式_第9张图片
image.png

  建造器比构造器的的一个次要优势是建造器可以有多个不定参数因为每个参数都是由它自己的方法指定的。或者,建造器可以将传递给方法的多个调用的参数聚合到单个字段中,如之前的addTopping方法所示。
  建造者模式相当灵活。一个单独的建造器可以被多次使用来build多个对象。在构建方法调用之前调整建造器的参数,可以改变创建的对象。
  建造者模式也有缺点。为了创建一个对象,你必须先创建它的建造器。虽然在实践中创建建造器的开销并没有那么明显,但是在性能瓶颈的情况下可能会出现问题。同样的,建造者模式比伸缩构造函数模式更冗长,所以它应该在有足够多的参数情况下才值得使用,比如四个或多个。但是记住你可能在将来会添加更多参数。但是如果你从构造方法或静态工厂开始,并在类演化到参数数量失控时转用建造器,那么过时的构造方法或静态工厂将拇指一样突出。因此,最好从一开始就使用建造器。
  总结,在设计类时,构造方法或静态工厂需要一只手数不过来的参数时,建造者模式是一个好选择,特别是很多参数都是可选的,类型也相同。
相比伸缩构造函数模式而言,客户端代码更容易读写,比JavaBean更安全。

本文写于2018.10.23,历时23天

你可能感兴趣的:(Effective Java(3rd)-Item2:当构造方法有很多参数的时候考虑使用建造者模式)