定义
The intent of the Builder design pattern is to separate the construction of a complex object from its representation. By doing so the same construction process can create different representations.
Builder设计模式的目的是将复杂对象的构造与其表示分离开来。通过这样做,相同的构造过程可以创建不同的表示。
使用场景
灵活在工作中运用设计模式是需要一定的功底的,首先我们需要知道它的使用场景:当一个类的构造函数参数个数超过4个,而且这些参数有些是可选的参数,考虑使用构造者模式。一个基本部件不会变,而其组合经常变化的时候,也可以考虑使用。
解决问题
考虑定义一个描述食品包装上的营养成分标签的类NutritionFacts。这些标签有几个必需的属性——每次建议的摄入量servingSize,每罐的份量servings和每份卡路里calories ,以及超过 20 个可选的属性——总脂肪、饱和脂肪、反式脂肪、胆固醇、钠等等。大多数产品只包含这些可选字段中的少数,且具有非零值(大部分字段为空)。
方式一
重载很多个构造方法,即折叠构造函数模式,因为我们不清楚未来在使用NutritionFacts的过程中会使用哪些参数来构建它的对象。
/**
* 描述食品包装上的营养成分标签
*/
public class NutritionFacts {
private final int servingSize; // (mL) required
private final int servings; // (per container) required
private final int calories; // (per serving) optional
private final int fat; // (g/serving) optional
private final int sodium; // (mg/serving) optional
private final int carbohydrate; // (g/serving) 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;
}
}
缺点: 阅读不便,例如public NutritionFacts(int servingSize, int servings) 中两个参数都是int类型,可能会存在参数传递混乱,但是这种情况编译器不会报错。且当参数例如上面要求的那么多,你不想那样写,我也不想那样写,说明这不是最优解。
方式二
当在构造方法中遇到许多可选参数时,另一种选择是 JavaBeans 模式,在这种模式中,调用一个无参的构造方法来创建对象,然后调用 setter 方法来设置每个必需的参数和可选参数:
public class NutritionFacts {
private int servingSize; // (mL) required
private int servings; // (per container) required
private int calories; // (per serving) optional
private int fat; // (g/serving) optional
private int sodium; // (mg/serving) optional
private int carbohydrate; // (g/serving) optional
public NutritionFacts(){}
public NutritionFacts(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public int getServingSize() {
return servingSize;
}
public void setServingSize(int servingSize) {
this.servingSize = servingSize;
}
public int getServings() {
return servings;
}
public void setServings(int servings) {
this.servings = servings;
}
public int getCalories() {
return calories;
}
public void setCalories(int calories) {
this.calories = calories;
}
public int getFat() {
return fat;
}
public void setFat(int fat) {
this.fat = fat;
}
public int getSodium() {
return sodium;
}
public void setSodium(int sodium) {
this.sodium = sodium;
}
public int getCarbohydrate() {
return carbohydrate;
}
public void setCarbohydrate(int carbohydrate) {
this.carbohydrate = carbohydrate;
}
在使用时可以先创建对象,再用set方法为其赋值。
**缺点:**构建过程中对象的状态容易发生变化,造成错误。因为NutritionFacts类中的属性是分步设置的,没有强制的一致性措施,所以就可能出错。
builder模式
- 在NutritionFacts 中创建一个静态内部类 Builder,然后将NutritionFacts中的参数都复制到Builder类中;
- 在NutritionFacts中创建一个private的构造函数,参数为Builder类型;
- 在Builder中创建一个public的构造函数,参数为NutritionFacts中必填的参数;
- 在Builder中创建设置函数,对NutritionFacts中那些可选参数进行赋值,返回值为Builder类型的实例;
- 在Builder中创建一个build()方法,在其中构建NutritionFacts的实例并返回.
/**
* 1. 在NutritionFacts 中创建一个静态内部类 Builder,然后将NutritionFacts中的参数都复制到Builder类中;
*/
public class NutritionFacts {
private final int servingSize; // (mL) required
private final int servings; // (per container) required
private final int calories; // (per serving) optional
private final int fat; // (g/serving) optional
private final int sodium; // (mg/serving) optional
private final int carbohydrate; // (g/serving) optional
/**
* 2. 在NutritionFacts中创建一个private的构造函数,参数为Builder类型;
*/
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 {
private final int servingSize; // (mL) required
private final int servings; // (per container) required
private int calories = 0; // (per serving) optional
private int fat = 0; // (g/serving) optional
private int sodium = 0; // (mg/serving) optional
private int carbohydrate = 0; // (g/serving) optional
/**
* 3. 在Builder中创建一个public的构造函数,参数为NutritionFacts中必填的参数;
*/
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
/**
* 4. 在Builder中创建设置函数,对NutritionFacts中那些可选参数进行赋值,返回值为Builder类型的实例;
*/
public Builder calories(int val) {
calories = val;
return this;
}
public Builder fat(int val) {
fat = val;
return this;
}
public Builder sodium(int val) {
sodium = val;
return this;
}
public Builder carbohydrate(int val) {
carbohydrate = val;
return this;
}
/**
* 5. 在Builder中创建一个build()方法,在其中构建NutritionFacts的实例并返回.
*/
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
//省略getter方法......
@Override
public String toString() {
return "NutritionFacts02{" +
"servingSize=" + servingSize +
", servings=" + servings +
", calories=" + calories +
", fat=" + fat +
", sodium=" + sodium +
", carbohydrate=" + carbohydrate +
'}';
}
}
如何在客户端使用
NutritionFacts 类是不可变的,所有的参数默认值都在一个地方。builder 的 setter 方法返回 builder 本身,这样就可以进行链式调用,从而生成一个流畅的 API。
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();
总而言之,当设计类的构造方法或静态工厂的参数超过几个时,Builder 模式是一个不错的选择,特别是许多参数是可选的或相同类型的。builder 模式客户端代码比使用伸缩构造方法(telescoping constructors)更容易读写,并且 builder 模式比 JavaBeans 更安全。
参考文章链接:
- https://github.com/wangdecheng/effective-java-3rd-chinese/blob/master/docs/notes/02.%20%E5%BD%93%E6%9E%84%E9%80%A0%E6%96%B9%E6%B3%95%E5%8F%82%E6%95%B0%E8%BF%87%E5%A4%9A%E6%97%B6%E4%BD%BF%E7%94%A8builder%E6%A8%A1%E5%BC%8F.md
- https://zhuanlan.zhihu.com/p/58093669