静态工厂和构造器有个共同的局限性:它们都不能很好地扩展到大量的可选参数。考虑用一个类表示包装食品外面显示的养分成份标签。这些标签中有几个域是必需的:每份的含量、每罐的含量以及每份的卡路里,还有超过20个可选域:总脂肪量、饱和脂肪量、转化脂肪、胆固醇、钠等等。大多数产品都只有几个可选域中会有非零的值。 java
对于这样的类,应该用哪一种构造器或者静态方法来编写呢?程序员一贯习惯采用telescoping constructor模式,在这种模式下,你提供一个只有必要参数的构造器,第二个构造器有一个可选参数,第三个有两个可选参数,依此类推,最后一个构造器包含全部可选参数。下面有个示例,为了简单起见,它只显示四个可选域:
node
// Telescoping constructor pattern - does not scale well! public class NutritionFacts { private final int servingSize; // (mL) required private final int servings; // (per container) required private final int calories; // optional private final int fat; // (g) optional private final int sodium; // (mg) optional private final int carbohydrate; // (g) optional 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。若是"仅仅"是这6个参数,看起来还不算太糟,问题是随着参数数目的增长,它很快就失去了控制。
一句话:telescoping constructor模式可行,可是当有许多参数的时候,客户端代码会很难编写,而且仍然较难以阅读。若是读者想知道那些值是什么意思,必须很仔细地数着这些参数来探个究竟。一长串相同的类型参数会致使一些微妙的错误。若是客户端不当心颠倒了这两种参数,编译器也不会出错,可是程序在运行时会出现错误的行为。
程序员
遇到许多构造器参数的时候,还有第二种代替办法,即JavaBeans模式,在这种模式下,调用一个无参构造器来建立对象,而后调用setter方法来设置每一个必要的参数,以及每一个相关的可选参数:
安全
// JavaBeans Pattern - allows inconsistency, mandates mutability public class NutritionFacts { // Parameters initialized to default values (if any) private int servingSize = -1; // Required; no default value private int servings = -1; // " " " " private int calories = 0; private int fat = 0; private int sodium = 0; private int carbohydrate = 0; public NutritionFacts() { } // Setters public void setServingSize(int val) { servingSize = val; } public void setServings(int val) { servings = val; } public void setCalories(int val) { calories = val; } public void setFat(int val) { fat = val; } public void setSodium(int val) { sodium = val; } public void setCarbohydrate(int val) { carbohydrate = val; } }这种模式不具有telescoping constructor模式的任何缺点。说得明白一点,就是建立实例很容易,这样产生的代码读起来也很容易:
NutritionFacts cocaCola = new NutritionFacts(); cocaCola.setServingSize(240); cocaCola.setServings(8); cocaCola.setCalories(100); cocaCola.setSodium(35); cocaCola.setCarbohydrate(27);
遗憾的是,JavaBeans模式自身有着很严重的缺点。由于构造过程被分到了几个调用中,在构造过程当中JavaBean可能处于不一致的状态。类没法仅仅经过检验构造器参数的有效性来保证一致性。试图使用处于不一致状态的对象,将会致使失败,这种失败与包含错误的代码截然不同,所以它调试起来十分困难。与此相关的另外一点不足在于,JavaBeans模式防止了把类作成不可变的可能(见第15条),这就须要程序员付出格外努力来确保它的线程安全。 性能
当对象的构造完成,而且不容许在解冻以前使用时,经过手工"冻结"对象,能够弥补这些不足,可是这种方式十分笨拙,在实践中不多使用。此外,它甚至会在运行时致使错误,由于编译器没法确保程序员会在使用以前先在对象上调用freeze方法。
ui
幸运的是,还有第三种替代方法,既能保证像telescoping constructor模式那样的安全性,也能保证像JavaBeans模式那么好的的可读性。这就是Builder模式[Gamma95,p.97]的一种形式。不直接生成想要的对象,而是让客户端利用全部必要的参数调用构造器(或者静态工厂),获得一个builder对象。而后客户端在builder对象上调用相似于setter的方法,来设置每一个相关的可选参数。最后,客户端调用无参的build方法来生成不可变的对象。这个builder是它构建的类的静态成员类(见第22条)。下面就是它的示例: this
// Builder Pattern 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; private NutritionFacts(Builder builder) { servingSize = builder.servingSize; servings = builder.servings; calories = builder.calories; fat = builder.fat; sodium = builder.sodium; carbohydrate = builder.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); } } }注意NutritionFacts是不可变的,全部的默认参数值都单独放在一个地方。builder的setter方法返回builder自己,以即可以把调用连接起来。下面就是客户端代码:
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8). calories(100).sodium(35).carbohydrate(27).build();这样的客户端代码很容易编写,更为重要的是,易于阅读。builder模式模拟了具名的可选参数,就像Ada和Python中的同样。
builder像个构造器同样,能够在它的参数中强加约束条件。build方法能够检验这些约束条件。将参数从builder拷贝到对象中以后,并在对象域而不是builder域(见第39条)中对它们进行检验,这个很重要。若是违反了任何约束条件,build方法就应该抛出IllegalStateException(见第60条)。异常的详细方法应该显示出违反了哪一个约束条件。
spa
对多个参数强加约束条件的另外一种方法是,用setter方法提供某个约束条件必须持有的全部参数。若是该约束条件没有获得知足,setter方法就会抛出IllegalArgumentException。这有个好处,就是一旦传递了无效的参数,当即就会发现约束条件失败,而不是等着调用build方法。
线程
与构造器相比,builder的一点微略优点在于,builder能够有多个varargs参数。构造器就像方法同样,只能有一个varargs参数。由于builder利用单独的方法来设置每一个参数,你想要多少个varargs参数,它们就能够有多少个,直到每一个setter方法都有一个varargs参数。
设计
Builder模式十分灵活,能够利用单个builder构建多个对象。builder的参数能够在建立对象期间进行调整,也能够随着不一样的对象而改变。builder能够自动填充某些域,例如每次建立对象时自动增长序列号。
设置了参数的builder生成了一个很好的抽象工厂(Abstract Factory)[Gamma95,p.87]。换句话说,客户端能够将这样一个builder传给方法,使该方法可以为客户端建立一个或者多个对象。要使用这种用法,须要有个类型来表示builder。若是使用的是发行版本1.5或者更新的版本,一个单独的泛型(见第26条)就能知足全部的builder,不管它们在构建哪一种类型的对象:
// A builder for objects of type T public interface Builder { public T build(); }注意,能够声明NutritionFacts.Builder类来实现Builder 。
带有Builder实例的方法一般利用有限制的通配符类型(bounded wildcard type,见第28条)来约束构建器的类型参数。例如,下面就是构建每一个节点的方法,它利用一个客户端提供的Builder实例构建树:
Tree buildTree(Builder nodeBuilder) { ... }Java中传统的抽象工厂实现是Class对象,用newInstance方法充当build方法的一部分。这种用法隐含着许多问题。newInstance方法老是企图调用类的无参构造器,这个构造器甚至可能根本不存在。若是类没有能够访问的无参构造器,你也不会收到编译时错误。相反,客户端代码必须在运行时处理InstantiationException或者IllegalAccessException,这样既不雅观也不方便。newInstance方法还会传播由无参构造器抛出的任何异常,即便newInstance缺少相应的throws子句。换句话说,Class.newInstance破坏了编译时的异常检查。上面讲过的Builder接口弥补了这些不足。
Builder模式的确也有它自身的不足。为了建立对象,必须先建立它的构建器。虽然建立构建器的开销在实践中可能不那么明显,可是在某些十分注重性能的状况下,可能就是个问题了。Builder模式还比telescoping constructor模式更加冗长,所以它只在有足够参数的时候才使用,好比4个或者更多个参数。可是记住,未来你可能须要添加参数。若是一开始就使用构造器或者静态工厂,等到类须要多个参数时才添加构建器,就会没法控制,那些过期的构造器或者静态工厂显得十分不协调。所以,一般最好一开始就使用构建器。
简而言之,若是类的构造器或者静态工厂中具备多个参数,设计这种类时,Builder模式就是种不错的选择,特别是当大多数参数都是可选的时候。与使用传统的telescoping constructor模式相比,使用Builder模式的客户端代码将更易于阅读和编写,builders也比JavaBeans更加安全。