2.Consider a builder when faced with many constructor parameters
大意为当你面对大量的构造参数时考虑使用Builder
静态工厂和构造器都有一个限制,它们不能够很好地缩减大量地选项参数,想象一下一种情况,你的类有着很多的成员变量,有些必须填写有些可以选填,那么如果使用传统的构造方法的话,排列组合一下可以想象会有多少个构造方法出现,这样的情况不是我们所需要的,程序员们通常会使用一种名为”伸缩构造函数模式(Telescoping constructor pattern )“的办法,就是先提供必须要的选项参数作为最简单的构造方法,然后把非必须的选项参数逐渐加上去构成新的构造方法,不考虑组合的问题
举个例子,现在你的构造方法有2个必须的参数A和B,然后有三个选填的参数C,D和E,那么如果我们使用Telescoping constructor pattern,那么代码看上去还是比较简洁的,只有4个不同的构造函数,如下
constructor(A a,B b){ //..... }
constructor(A a,B b,C c){ //..... }
constructor(A a,B b,C c,D d){ //..... }
constructor(A a,B b,C c,D d,E e){ //..... }
这样出现的问题很明显,当你想使用A,B和E作为参数的时候,你不得不填上其他的所有参数,也就是C和D你也必须填上去
好吧,可能多填两个参数你还是可以接受的话,不妨想象一下我们现在有着上百个参数,那么麻烦就大了
简要的总结一下,伸缩构造函数的模式的确起作用了,但是这对于代码编写和阅读仍然有着一定的困难
这样情况下,我们在使用的时候可能会因为参数列表过长,然后不小心相互位置放错而导致程序炸了,这在编译阶段可能看不出来错误
在你面对这样许多参数的情况,有一种方法叫做JavaBeans的模式,这个模式很简单,就是你只需要构造一个含所必需参数的构造函数,其他的选项都使用setter来设置即可,当然你的参数都是private的
这样的模式消灭了伸缩构造带来的烦恼,很简单去实现,而且易于阅读
但是这样的模式也存在着或多或少的问题,因为构造会在多次反复调用中分裂,一个JavaBean 可能在他的构造中是不一致的状态,什么意思呢,就是说你如果使用JavaBean 那么你所构造的类的参数是否完整并不是必须的,而且参数可能之前没有,过一段代码流程你又添加了,这就是不一致性,你所构造的类可能是缺少参数的,但是我们在调用一些方法的时候并不会去检查这些参数的存在性,那么就可能导致问题的出现,debug起来可能也较为困难
还有一个JavaBean模式的问题就是,这一种模式排除了使一个类变成不变的类(Immutable Class)的可能性,而且需要在程序员保重线程安全的部分做出额外的努力
这些缺点呢,我们可以当构造结束时手动地”冰冻“(freezing)这些对象并且不允许被它使用直到它被解冻来减少这些缺点,当然这个方法也有许多问题存在,比如编译器并不能确定你所使用地方法是否被冻结了。
幸运的是,这里有第三种解决方案,既包括了伸缩构造模式的安全性又有JavaBeans模式的可读性。它就是Builder模式,并非直接地创建一个需要的对象,用户先调用一个需要全部必需参数的构造方法,然后得到一个builder对象,接着用户使用类似setter的方法来在builder上设置参数,最后调用build方法来生成对象,这样生成的对象是immutable(不可变的),builder在它所build的类中是一个静态的成员类
这里给出书中的例子
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public static class Builder {
// Required parameters
private final int servingSize;
private final int servings;
// Optional parameters - initialized to default values
private int calories = 0;
private int fat = 0;
private int carbohydrate = 0;
private int sodium = 0;
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val)
{ calories = val; return this; }
public Builder fat(int val)
{ fat = val; return this; }
public Builder carbohydrate(int val)
{ carbohydrate = val; return this; }
public Builder sodium(int val)
{ sodium = val; return this; }
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
从例子中我们可以看出,这一模式就是利用Builder类来初始化参数,设置参数,然后再把自己作为参数传入主类的构造函数中,并且给参数赋值实现对象的建立,注意其中的类似于setter的设置,返回的是this,所以可以使用链式的调用,比如
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).
calories(100).sodium(35).carbohydrate(27).build();
这样的程序语句易于我们去编写,更重要的是,容易读,Builer模式模拟命名了选项参数可以在Python或者Ada中看到它的踪影
就像一个构造方法一样,一个Builder能够强加不变的性质在它的参数上,build方法可以检查这些不变量,重要的是,它们在复制参数从builder到对象之后会被检查,是在对象的域进行检查。如果有不变量是冲突的,build会抛出一个名为 IllegalStateException的异常,这个方法会提示哪一个不变量是冲突的
在多个参数中强加不变量的另一个方法是使用setter方法设置整个参数组,如果不变量不满足要求,那么setter方法就会抛出一个名为IllegalArgumentException的异常
对于builder来说,一个次要的优点是builder可以拥有多个变量参数。而构造方法只能有一个变量参数,因为builder使用分离的多个方法来设置相应的参数(解释一下,构造方法,或者说方法的变量参数只能是一个 比如 A(int a){},就不能是A(int a1 a2 a3){}这样)
Builder模式十分灵活,一个builder可以被用来build多个对象,builder的参数可以被调整使得对象不同,builder可以自动的补充某个域,在对象生成的时候会自动产生一系列的数字
一个所拥有参数被事先设置的builder构成一个良好的抽象工厂(Abstract Factory),换句话说,我们可以通过这样一个builder来变成一个方法去创建多个对象,为了实现这样的使用方案,我们需要一个类型来表达builder
// A builder for objects of type T
public interface Builder<T> {
public T build();
}
拥有这一个Builder实例地方法会特别地约束builder的类型参数,这个类型参数使用有界通配符,举个例子,这里有个使用用户提供的Builder实例来创建相对应节点来创建一棵树的方法
Tree buildTree(Builder<? extends Node> nodeBuilder) { ... }
传统的抽象工厂在Java上的实现曾经是一个类的对象,有着newInstance方法,这个方法起到了build方法的作用。这样的用法有着问题,这个newInstance方法呢经常企图调用类的无参构造方法,但这个无参的构造方法可能并不存在,当这个类没有可用的无参构造方法的时候你不会在编译阶段得到一个error,那么应对这个问题我们使捕获InstantiationException或者IllegalAccessException来解决,但是这样太丑了而且不方便。Class.newInstance 破坏了编译阶段exception的检查,使用Builder接口就可以解决这些缺陷
当然Builder模式也是有缺点的,创建一个类的时候你必须先创建builder,你必须确定一下创建一个builder的代价开销,在某些情况下可能是个重要的问题。当然builder对于伸缩构造模式来说更为详细,它只创建你需要的参数下的对象,当然参数足够多建议使用builder,否则可能没有什么意义,如果你的参数有4个或者更多而且后期可能继续添加,请第一时间想到使用builder模式作为类编写的开始。
总结,Builder模式当我们设计一个有着许多需要处理的参数的类的时候是一个好的选择,特别是其中的许多参数都是可选的,我们的代码使用builders比使用传统伸缩构造模式更加易于读和写,比起JavaBeans更加安全。