首页 建造者模式
文章
取消

建造者模式

为什么需要建造者模式

已知建造者模式是用来创建对象的,工厂模式也能创建对象,或者直接使用构造函数或者配合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 类从所需对象中完全移除,并且适应一大批相似结构对象的创建需求。

本文由作者按照 CC BY 4.0 进行授权

工厂模式

Junit的小demo