构造器与静态工厂方法的局限性
本小节介绍了构建对象的第三种方法--构造器(Builder)模式。首先作者回顾了公共构建器和静态工厂方法共同的缺陷--对于有大量可选属性类的构建存在问题。
首先无论是静态工厂方法还是公共构造器,都会面临可选参数设置的问题,一般的解决方式就是重叠构造器,定义N个构造器(或者静态工厂方法),通过枚举的方式穷尽所有可选参数的设置情况,这样当可选参数很多的时候就会失去控制。
解决重叠构造器数量失控问题的一个思路是采用JavaBeans的模式,这种模式下只需要定义一个无参构造函数或者只初始化必要属性的构造函数,其他可选参数都通过setter方法来设置。这个方法带来了两个新的问题,一是使得设置不可变类的可能性基本丧失(因为所有可选变量都必须在类加载时初始化,同时要满足被setter方法调用的需求),二是该方法的线程安全存在问题,需要在使用时考虑一些线程安全方法,会显得非常笨拙。
Builder模式能够解决不可变类的问题(虽然不能直接解决线程安全的问题,但是由于允许了不可变类也算是间接把线程安全问题解决了)。
什么是构造器(Builder)
构造器模式的本质是在目标类中设置一个静态成员变量(Builder),将目标类的各种属性先在这个静态成员变量中设置好,然后通过这个Builder的build方法去构建目标类的对象。这个方式第一允许了目标类属性使用final标识,解决了不可变类的问题;第二属性的设置方法模拟了静态工厂方法的具名特性,使得更易阅读和使用。
使用构建器的案例
第一个例子是应用在一个无层次的NutritionFacts类上:
- NutritionFacts类要是一个不可变类
- NutritionFacts有2个必选属性(servingSize、servings)和4个可选参数(calories、fat、sodium、carbohydrate),2个必选参数无初始值、4个可选参数初始值为0。
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 { private final int servingSize; private final int servings; private int calories = 0; private int fat = 0; private int sodium = 0; private int carbohydrate = 0; public Builder(int servingSize, int servings){ this.servingSize = servingSize; this.servings = servings; } public Builder calories(int val){ this.calories = val; return this; } public Builder fat(int val){ this.fat = val; return this; } public Builder sodium(int val){ this.sodium = val; return this; } public Builder carbohydrate(int val){ this.carbohydrate = val; return this; } public NutritionFacts build(){ return new NutritionFacts(this); } } private NutritionFacts(Builder builder){ this.servingSize = builder.servingSize; this.servings = builder.servings; this.calories = builder.calories; this.sodium = builder.sodium; this.fat = builder.fat; this.carbohydrate = builder.carbohydrate; } }
第二个例子是应用在一个有层次的类族上
- 抽象类Pizza包含一个pizza topping 的选项(用枚举类实现)
- 实现类NyPizza可以按照“大、中、小”选择尺寸(用枚举类实现)
- 因为实现类有自己特色的属性(size),所以抽象类和子类都要拥有自己独立的Builder,但是子类的Builder需要继承自抽象类Pizza的Builder
Pizza抽象类
import java.util.EnumSet;
import java.util.Set;
public abstract class Pizza {
public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE };
public Set<Topping> toppings;
abstract static class Builder<T extends Builder<T>> {
public EnumSet toppings = EnumSet.noneOf(Topping.class);
public T addTopping(Topping topping){
toppings.add(topping);
return self();
}
abstract Pizza build();
public abstract T self();
}
protected Pizza(Builder builder)
{
toppings = builder.toppings.clone();
}
}
这里有一个细节,就是Pizza的构造器做了一个保护性拷贝,使得Pizza的topping set独立拷贝一份数据(而不是仅仅通过对象引用),这保证了topping后续操作的安全。
NyPizza子类
import java.util.Objects;
public class NyPizza extends Pizza {
public enum Size {SMALL, MEDIUM, LARGE};
private final Size size;
public static class Builder extends Pizza.Builder<Builder>{
private final Size size;
public Builder(Size size)
{
this.size = Objects.requireNonNull(size);
}
@Override
public Builder self()
{
return this;
}
@Override
public NyPizza build()
{
return new NyPizza(this);
}
}
private NyPizza(Builder builder)
{
super(builder);
size = builder.size;
}
}
注:这里MyPizza构造器的size属性没有使用拷贝是因为无论builder类里还是NyPizza里的Size属性都是不可变(final)。
客户端
package BuilderModel.Pizza;
import BuilderModel.Pizza.NyPizza.Size;
public class PizzaApplication {
public static void main(String[] args) {
Pizza pizza = new NyPizza.Builder(Size.SMALL)
.addTopping(Pizza.Topping.PEPPER)
.addTopping(Pizza.Topping.SAUSAGE)
.build();
System.out.println(pizza);
}
}