阅读经典——《Effective Java》02
若一个类的构造器参数多达5个以上时该怎么办?无论是静态工厂还是构造器都有明显的局限性,它们不能很好地扩展到大量可选参数。
- 解决方案
- Builder模式
解决方案
首先提出一个真实案例:用一个类表示食品包装上标注的营养成分标签。这些标签中,每份含量、每罐含量和每份卡路里是必须的;另外还有多个可选域,总脂肪量、饱和脂肪量、转化脂肪、胆固醇、钠等等。
有三种解决方案可以处理这种问题。
- 采用重叠构造器
- JavaBeans模式
- Builder模式
第一种方案重叠构造器在参数个数不是非常多的时候倒是很常用的方法,后一个构造器比前一个构造器多一个参数,前一个构造器调用后一个构造器。但是参数过多并且带有可选参数时,重叠构造器就显得不那么灵活了,如何安排参数顺序也成了难题。
第二种方案JavaBeans模式从形式上来说非常适合于大量参数,用起来很直观。构造器不传任何参数或只传少量的必要参数,其它参数通过setter方法传入。但该方案具有明显的缺点,参数传入是一个个分离的过程,无法保证它们的完整性,必须由用户自己保证传入合适的参数。而且,JavaBeans模式导致该类无法成为不可变类(因为不可变类不能提供setter方法)。
第三种方案才是本文将要着重介绍的Builder模式。该模式既能保证重叠构造器那样的安全性,又能保证JavaBeans模式那样的可读性。
Builder模式
在Builder模式中,不调用构造器生成对象,而是使用静态工厂方法得到一个builder对象,然后在builder上调用类似于setter的方法,设置需要的参数,最后调用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 NutitionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbodydrate = builder.carbohydrate;
}
}
可以看到,NutritionFacts类是一个不变类,它的所有字段(也叫域)都被final
修饰。Builder是它的静态内部类,提供了必选字段作为参数的构造器,以及可选字段的setter方法。巧妙的是,这些setter方法,包括calories
、fat
、carbohydrate
、sodium
,的返回值都是this
引用,因此可以在同一个builder对象上形成调用链,如下面的客户端代码。
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100)
.sodium(35)
.carbohydrate(27)
.build();
对于任意多的可选参数,都在Builder类中设置了缺省值,调用链中可以任意使用多个setter方法。而且Builder模式非常灵活,参数可以在Builder内部做进一步处理,比如有效性检查。
当然,Builder模式也有自身的不足。为了创建对象必须先创建相应的Builder对象,这会增加代码的复杂度,而且带来稍微的性能损失。我们应该在参数很多的时候才使用该模式。但是请记住,程序总是要扩展的,一旦将来需要添加参数而一开始没有使用Builder模式,在保持兼容的情况下修改代码将是非常难以控制的。因此,最好一开始就使用Builder。
关注作者或文集《Effective Java》,第一时间获取最新发布文章。