为什么需要建造者模式
已知建造者模式是用来创建对象的,工厂模式也能创建对象,或者直接使用构造函数或者配合set方法也能创建对象,那么为什么还需要建造者模式呢?建造者模式适合什么样的应用场景呢?
事实上静态工厂和构造函数都有一个局限:它们不能对大量可选参数做很好的扩展。
举个例子:一个类表示包装食品上的营养标签。这些标签上有一些字段是必需的,如:净含量、毛重和每单位份量的卡路里,另有超过 20 个可选的字段,如:总脂肪、饱和脂肪、反式脂肪、胆固醇、钠等等。大多数产品只有这些可选字段中的少数,且具有非零值。
对于这样的类什么样的创建方式是合适的呢?目前有两种方式:
第一种-构造器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class NutritionFacts {
private final int servingSize; // 净含量 required
private final int servings; // 毛重 required
private final int calories; // 卡路里 optional
private final int fat; // 脂肪 optional
private final int sodium; // 钠 optional
private final int carbohydrate; // 碳水化合物 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;
}
}
当你现在想要创建一个对象时你可以这样:
1
NutritionFacts cocaCola =new NutritionFacts(240, 8, 100, 0, 35, 27);
虽然现在看起来没什么问题,构造器的可选参数最多有 6 个,但如果新的业务场景希望 NutritionFacts 包含剩下的 20 个可选参数,我想使用这种方式去创建对象的开发者一定会疯掉。(为什么?因为他至少需要写 20 个这样的构造器,并且如果参数类型相同,调用者就容易传递错误的参数,虽然编译器不会报错,但程序可能在运行时出错!)但是幸好我们还有另外一种方式:
第二种-Setter
在这种模式中,可以调用一个只传必传参数的构造函数来创建对象,然后调用 setter 方法来设置每个感兴趣的可选参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class NutritionFacts {
private int servingSize; // Required
private int servings; // Required
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public NutritionFacts(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
// 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; }
}
相对于构造器的方式,Setter 明显就不用编写更多的构造器了,可读性和易用性也强于构造器方式,虽然创建对象的方式会显得冗余:
1
2
3
4
NutritionFacts cocaCola = new NutritionFacts(240, 8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
遗憾的是这种方式也存在巨大的缺陷
实际上采用这种先创建后 set 的构建方式,JavaBean 容易处在不一致的状态:
1
2
3
4
NutritionFacts cocaCola = new NutritionFacts(240, 8); //invalid
cocaCola.setCalories(100); //invalid
cocaCola.setSodium(35); //invalid
cocaCola.setCarbohydrate(27); //valid
在不一致的状态下尝试使用对象可能会导致错误的发生,为了避免这种情况,就应该让对象一次性创建出来,但是这种方式它天然的做不到。同时又因为暴露了修改内部状态的 Setter 方式,想要生成的对象是不可变对象,也做不到!
还好方法总比困难多,除了上面的 A 计划、B 计划,我们还有 Plan C!look this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 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;
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 sodium = 0;
private int carbohydrate = 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 sodium(int val) {
sodium = val;
return this;
}
public Builder carbohydrate(int val) {
carbohydrate = 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 对象(build 通常是它构建的类的静态成员,当然也可以独立成为一个类),然后调用像 Setter 一样的方法设置每个感兴趣的可选参数,不过这种方法通常返回 builder 对象,最后客户端调用一个无参数的构建方法 build() 来生成所需对象,就像下面这样:
1
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).carbohydrate(27).build();
这样的客户端代码易于编写,更加易于阅读,并且创建的对象是不可变的(因为所需对象的构造器是 private,并且只接受 builder 对象参数,然后没有暴露 Setter)而且是一次性创建,所以也不会存在不一致的情况,简直完美解决了另外两种方法的缺点。
当然这里举得例子忽略了参数的非法校验,比如显示指定了 param1,就应该显示指定 param2 和 param3,或者 parame2 和 param3 都应该小于等于 param1。这样我们可以统一在最后的 build() 方法内部进行校验,就像下面这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
public ResourcePoolConfig build() {
// 校验逻辑放到这里来做,包括必填项校验、依赖关系校验、约束条件校验等
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("...");
}
if (maxIdle > maxTotal) {
throw new IllegalArgumentException("...");
}
if (minIdle > maxTotal || minIdle > maxIdle) {
throw new IllegalArgumentException("...");
}
return new ResourcePoolConfig(this);
}
上面的校验包括:名称(name)不能为空,最大空闲资源数量(maxIdle)不能大于最大总资源数量(maxTotal),最小空闲资源数量(minIdle)不能大于最大总资源数量(maxTotal),还有最小空闲资源数量(minIdle)不能大于最大空闲资源数量(maxIdle)。
建造者模式的缺点
由于使用建造者模式时,每次客户端创建所需对象时需要首先创建 builder 对象,虽然在实际应用中创建这个构建器的成本可能并不显著,但在以性能为关键的场景下,这可能会是一个问题。
而且建造者的代码有点重复,builder 的状态变量与它构建的对象是一样的,同时相比构造器方法,builder 的客户端代码会更加的冗余些。
其实我觉得建造者的缺点都不算缺点,假使你用 Constructor or Setter 都不能满足客户端创建所需对象的要求,那么大胆的选择建造者模式吧,它就是为此而生的!而且针对代码重复的问题,也不是没有解决办法,像下面这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class EntityCreator<T> {
private Class<T> classInstance;
private T entityObj;
public EntityCreator(Class<T> classInstance, Object... initParams) throws Exception {
this.classInstance = classInstance;
//这段 for 循环就是为了匹配对应的构造器,没什么其他作用
Class<?>[] paramTypes = new Class<?>[initParams.length];
for (int index = 0, length = initParams.length; index < length; index++) {
String checkStr = initParams[index].getClass().getSimpleName();
if (checkStr.contains("Integer")) {
paramTypes[index] = int.class;
}
if (checkStr.contains("Double")) {
paramTypes[index] = double.class;
}
if (checkStr.contains("Boolean")) {
paramTypes[index] = boolean.class;
}
if (checkStr.contains("String")) {
paramTypes[index] = initParams[index].getClass();
}
}
//匹配正确的构造器创建出对象
Constructor<T> constructor = classInstance.getDeclaredConstructor(paramTypes);
constructor.setAccessible(true);
this.entityObj = constructor.newInstance(initParams);
}
public EntityCreator<T> setValue(String paramName, Object paramValue) throws Exception {
Field field = classInstance.getDeclaredField(paramName);
field.setAccessible(true);
field.set(entityObj, paramValue);
return this;
}
public T build() {
return entityObj;
}
}
这个方法适合那些没有独特校验规则,并且适合用建造者模式创建的对象。这样就可以将 builder 类从所需对象中完全移除,并且适应一大批相似结构对象的创建需求。