文章作者:Tyan
博客:noahsnail.com
Item 2: Consider a builder when faced with many constructor parameters
Item 2:当面临很多构造函数参数时,要考虑使用构建器
Static factories and constructors share a limitation: they do not scale well to large numbers of optional parameters. Consider the case of a class representing the Nutrition Facts label that appears on packaged foods. These labels have a few required fields—serving size, servings per container, and calories per serving and over twenty optional fields—total fat, saturated fat, trans fat, cholesterol, sodium, and so on. Most products have nonzero values for only a few of these optional fields.
静态工厂和构造函数有一个共同的限制:对于大量可选参数它们都不能很好的扩展。考虑这样一种情况:用一个类来表示包装食品上的营养成分标签。这些标签有几个字段是必须的——每份含量、每罐含量(份数)、每份的卡路里,二十个以上的可选字段——总脂肪量、饱和脂肪量、转化脂肪、胆固醇、钠等等。大多数产品中这些可选字段中的仅有几个是非零值。
What sort of constructors or static factories should you write for such a class? Traditionally, programmers have used the telescoping constructor pattern, in which you provide a constructor with only the required parameters, another with a single optional parameter, a third with two optional parameters, and so on, culminating in a constructor with all the optional parameters. Here’s how it looks in practice. For brevity’s sake, only four optional fields are shown:
你应该为这样的一个类写什么样的构造函数或静态工厂?习惯上,程序员使用重叠构造函数模式,在这种模式中只给第一个构造函数提供必要的参数,给第二个构造函数提供一个可选参数,给第三个构造函数提供两个可选参数,以此类推,最后的构造函数具有所有的可选参数。下面是一个实践中的例子。为了简便,只显示了四个可选字段:
1 | //Telescoping constructor pattern - does not scale well! |
When you want to create an instance, you use the constructor with the shortest parameter list containing all the parameters you want to set:
当你想创建一个实例时,你可以使用具有最短参数列表的构造函数,最短参数列表包含了所有你想设置的参数:
1 | NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27); |
Typically this constructor invocation will require many parameters that you don’t want to set, but you’re forced to pass a value for them anyway. In this case, we passed a value of 0 for fat. With “only” six parameters this may not seem so bad, but it quickly gets out of hand as the number of parameters increases.
通常构造函数调用需要许多你不想设置的参数,但无论如何你不得不为它们传值。在这种情况下,我们给fat
传了一个零值。只有六个参数可能还不是那么糟糕,但随着参数数目的增长它很快就会失控。
In short, the telescoping constructor pattern works, but it is hard to write client code when there are many parameters, and harder still to read it. The reader is left wondering what all those values mean and must carefully count parameters to find out. Long sequences of identically typed parameters can cause subtle bugs. If the client accidentally reverses two such parameters, the compiler won’t complain, but the program will misbehave at runtime (Item 40).
简而言之,重叠构造函数模式有作用,但是当有许多参数时很难编写客户端代码,更难的是阅读代码。读者会很奇怪所有的这些值是什么意思,必须仔细的计算参数个数才能查明。一长串同类型的参数会引起细微的错误。如果客户端偶然的颠倒了两个这样的参数,编译器不会报错,但程序在运行时会出现错误的行为(Item 40)。
A second alternative when you are faced with many constructor parameters is the JavaBeans pattern, in which you call a parameterless constructor to create the object and then call setter methods to set each required parameter and each optional parameter of interest:
当你面临许多构造函数参数时,第二个替代选择是JavaBeans模式,在这种模式中你要调用无参构造函数来创建对象,然后调用setter
方法为每一个必要参数和每一个有兴趣的可选参数设置值:
1 | //JavaBeans Pattern - allows inconsistency, mandates mutability |
This pattern has none of the disadvantages of the telescoping constructor pattern. It is easy, if a bit wordy, to create instances, and easy to read the resulting code:
这个模式没有重叠构造函数模式的缺点。即使有点啰嗦,但它很容易创建实例,也很容易阅读写出来的代码:
1 | NutritionFacts cocaCola = new NutritionFacts(); |
Unfortunately, the JavaBeans pattern has serious disadvantages of its own. Because construction is split across multiple calls, a JavaBean may be in an inconsistent state partway through its construction. The class does not have the option of enforcing consistency merely by checking the validity of the constructor parameters. Attempting to use an object when it’s in an inconsistent state may cause failures that are far removed from the code containing the bug, hence difficult to debug. A related disadvantage is that the JavaBeans pattern precludes the possibility of making a class immutable (Item 15), and requires added effort on the part of the programmer to ensure thread safety.
遗憾的是,JavaBeans模式自身有着严重缺点。因为构造过程跨越多次调用,JavaBean在构造过程中可能会出现不一致的状态。JavaBean类不能只通过检查构造函数参数的有效性来保证一致性。当一个对象处于一种不一致的状态时,试图使用它可能会引起失败,这个失败很难从包含错误的代码中去掉,因此很难调试。与此相关的一个缺点是JavaBeans模式排除了使一个类不可变的可能性*(Item 15),因此需要程序员付出额外的努力来确保线程安全。
It is possible to reduce these disadvantages by manually “freezing” the object when its construction is complete and not allowing it to be used until frozen, but this variant is unwieldy and rarely used in practice. Moreover, it can cause errors at runtime, as the compiler cannot ensure that the programmer calls the freeze method on an object before using it.
当构造工作完成时,可以通过手动『冰冻』对象并且在冰冻完成之前不允许使用它来弥补这个缺点,但这种方式太笨重了,在实践中很少使用。而且,由于编译器不能保证程序员在使用对象之前调用了冰冻方法,因此它可能在运行时引起错误。
Luckily, there is a third alternative that combines the safety of the telescoping constructor pattern with the readability of the JavaBeans pattern. It is a form of the Builder pattern [Gamma95, p. 97]. Instead of making the desired object directly, the client calls a constructor (or static factory) with all of the required parameters and gets a builder object. Then the client calls setter-like methods on the builder object to set each optional parameter of interest. Finally, the client calls a parameterless build method to generate the object, which is immutable. The builder is a static member class (Item 22) of the class it builds. Here’s how it looks in practice:
幸运的是,这儿还有第三种替代方法,它结合了重叠构造函数模式的安全性和JavaBeans模式的可读性。它就是构建器模式[Gamma95, p. 97]。它不直接构建需要的对象,客户端调用具有所有参数的构造函数(或静态工厂),得到一个构造器对象。然后客户端在构建器上调用类似于setter的方法来设置每个感兴趣的可选参数。最终,客户端调用无参构建方法来产生一个对象,这个对象是不可变的。构建器是它要构建的类的静态成员类(Item 22)。它在实践中的形式如下:
1 | //Builder Pattern |
Note that NutritionFacts
is immutable, and that all parameter default values are in a single location. The builder’s setter methods return the builder itself so that invocations can be chained. Here’s how the client code looks:
注意NutritionFacts
是不可变的,所有参数的默认值都在一个单独的位置。构建器的setter
方法返回的是构建器本身,为的是可以链式调用。客户端代码如下:
1 | NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).carbohydrate(27).build(); |
This client code is easy to write and, more importantly, to read. The Builder pattern simulates named optional parameters as found in Ada and Python.
Like a constructor, a builder can impose invariants on its parameters. The build method can check these invariants. It is critical that they be checked after copying the parameters from the builder to the object, and that they be checked on the object fields rather than the builder fields (Item 39). If any invariants are violated, the build method should throw an IllegalStateException
(Item 60). The exception’s detail method should indicate which invariant is violated (Item 63).
客户端代码很容器写,更重要的是很容易读。构建器模式模拟了命名可选参数,就像Ada和Python中的一样。类似于构造函数,构造器可以对它参数加上约束条件。构造器方法可以检查这些约束条件。将参数从构建器拷贝到对象中之后,可以在对象作用域而不是构造器作用域对约束条件进行检查,这是很关键的(Item 39)。如果违反了任何约束条件,构造器方法会抛出IllegalStateException
异常(Item 60)。异常的详细信息会指出违反了哪一个约束条件(Item 63)。
Another way to impose invariants involving multiple parameters is to have setter methods take entire groups of parameters on which some invariant must hold. If the invariant isn’t satisfied, the setter method throws an IllegalArgumentException
. This has the advantage of detecting the invariant failure as soon as the invalid parameters are passed, instead of waiting for build
to be invoked.
给许多参数加上约束条件的另一种方式是对某些约束条件必须持有的整组参数用setter方法进行检查,如果没有满足约束条件,setter方法会抛出IllegalArgumentException
异常。这个优点在于是一旦传递了无效参数,检测约束条件会失败,而不是等待build
被调用。
A minor advantage of builders over constructors is that builders can have multiple varargs parameters. Constructors, like methods, can have only one varargs parameter. Because builders use separate methods to set each parameter, they can have as many varargs parameters as you like, up to one per setter method.
相比于构造函数,构建器的一个小优势在与构建器可以有许多可变参数。构造函数类似于方法,只能有一个可变参数。由于构造器用单独的方法设置每一个参数,因此像你喜欢的那样,它们能有许多可变参数,直到每个setter方法都有一个可变参数。
The Builder pattern is flexible. A single builder can be used to build multiple objects. The parameters of the builder can be tweaked between object creations to vary the objects. The builder can fill in some fields automatically, such as a serial number that automatically increases each time an object is created.
构建器模式是灵活的。一个构建器可以用来构建多个对象。为了改变对象,构建器参数在创建对象时可以进行改变。构建器能自动填充一些字段,例如每次创建对象时序号自动增加。
A builder whose parameters have been set makes a fine Abstract Factory [Gamma95, p. 87]. In other words, a client can pass such a builder to a method to enable the method to create one or more objects for the client. To enable this usage, you need a type to represent the builder. If you are using release 1.5 or a later release, a single generic type (Item 26) suffices for all builders, no matter what type of object they’re building:
设置了参数的构建器形成了一个很好的抽象工厂[Gamma95,p.87]。换句话说,为了使某个方法能为客户端创建一个或多个对象,客户端可以传递这样的一个构建器到这个方法中。为了使这个用法可用,你需要用一个类型来表示构建器。如果你在使用JDK 1.5或之后的版本,只要一个泛型就能满足所有的构建器(Item 26),无论正在构建的是什么类型:
1 | // A builder for objects of type T |
Note that our NutritionFacts.Builder
class could be declared to implement Builder<NutritionFacts>
.
注意我们可以声明NutritionFacts.Builder
类来实现Builder<NutritionFacts>
。
Methods that take a Builder instance would typically constrain the builder’s type parameter using a bounded wildcard type (Item 28). For example, here is a method that builds a tree using a client-provided Builder instance to build each node:
带有构建器实例的方法通常使用绑定的通配符类型来约束构建器的类型参数(Item 28)。例如,构建树的方法通过使用客户端提供的构建器实例来构建每一个结点:
1 | Tree buildTree(Builder<? extends Node> nodeBuilder) { ... } |
The traditional Abstract Factory implementation in Java has been the Class object, with the newInstance
method playing the part of the build
method. This usage is fraught with problems. The newInstance
method always attempts to invoke the class’s parameterless constructor, which may not even exist. You don’t get a compile-time error if the class has no accessible parameterless constructor. Instead, the client code must cope with InstantiationException
or IllegalAccessException
at runtime, which is ugly and inconvenient. Also, the newInstance
method propagates any exceptions thrown by the parameterless constructor, even though newInstance
lacks the corresponding throws clauses. In other words, Class.newInstance
breaks compile-time exception checking. The Builder
interface, shown above, corrects these deficiencies.
Java中传统的抽象工厂实现是类对象,newInstance
方法扮演着build
方法的角色。 这种用法问题重重。newInstance
方法总是尝试调用类的无参构造函数,但无参构造函数可能并不存在。如果类没有访问无参构造函数,你不会收到编译时错误。而客户端代码必须处理运行时的InstantiationException
或IllegalAccessException
异常,这样既不雅观也不方便。newInstance
也会传播无参构造函数抛出的任何异常,即使newInstance
缺少对应的抛出语句块。换句话说,Class.newInstance
打破了编译时的异常检测。上面的Builder
接口弥补了这些缺陷。
The Builder pattern does have disadvantages of its own. In order to create an object, you must first create its builder. While the cost of creating the builder is unlikely to be noticeable in practice, it could be a problem in some performance-critical situations. Also, the Builder pattern is more verbose than the telescoping constructor pattern, so it should be used only if there are enough parameters, say, four or more. But keep in mind that you may want to add parameters in the future. If you start out with constructors or static factories, and add a builder when the class evolves to the point where the number of parameters starts to get 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.
构建器模式也有它的缺点。为了创建对象,你必须首先创建它的构建器。虽然创建构建器的代价在实践中可能不是那么明显,但在某些性能优先关键的情况下它可能是一个问题。构建器模式比重叠构造函数模式更啰嗦,因此只有在参数足够多的情况下才去使用它,比如四个或更多。但要记住将来你可能会增加参数。如果你开始使用构造函数或静态工厂,当类发展到参数数目开始失控的情况下,才增加一个构建器,废弃的构造函数或静态工厂就像一个疼痛的拇指,最好是在开始就使用构建器。
In summary, the Builder pattern is a good choice when designing classes whose constructors or static factories would have more than a handful of parameters, especially if most of those parameters are optional. Client code is much easier to read and write with builders than with the traditional telescoping constructor pattern, and builders are much safer than JavaBeans.
总之,当设计的类的构造函数或静态工厂有许多参数时,构建器模式是一个很好的选择,尤其是大多数参数是可选参数的情况下。与传统的重叠构造函数模式相比,使用构建器模式的客户端代码更易读易编写,与JavaBeans模式相比使用构建器模式更安全。