Effective-Java-2遇到多个构造器参数时需要考虑用构建器

静态工厂和构造器有个共同的局限性:都不能很好地扩展大量的可选参数

例:

考虑用一个类表示包装食品外面显示的营养成分标签。这些标签中有几个域是必需的:每份含量,每罐含量,每份卡路里,还有超过20个可选域:总脂肪量,饱和脂肪量,转化脂肪量,胆固醇,钠等等。大多数的产品在某几个可选域中都会有非0的值。

对于这样的类,应该用哪种构造器或者静态方法来编写呢?

1. 重叠构造器 (telescoping constructor)

提供第一个只有必要参数的构造器

提供第二个包含必要参数且包含一个可选参数的构造器

提供第三个包含必要参数且包含两个可选参数的构造器

以此类推,最后一个构造器包含所有参数

如下是个简单的示例

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
public class NutritionFacts {
private final int servingSize; //(ml) required
private final int servings; //(per container) required
private final int calories; // optional
private final int fat; //(g) optional
private final int sodium; //(mg) optional
private final int carbohydrate; //(g) 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);

如上初始化中 fat 的值为0,这个参数本是不用初始化的,就6个参数的情况下,还说的过去,随着参数增加,这样就不行了。

重叠构造器是可行的,但当参数增多时,客户端的代码将变得很难编写,阅读性也较差,并且如果初始化时容易填错顺序,导致运行时的错误。

2. JavaBeans模式

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
// JavaBeans Pattern - allows inconsistency, mandates mutability
public class NutritionFacts {
private int servingSize = -1; //(ml) required
private int servings = -1; //(per container) required
private int calories = 0; // optional
private int fat = 0; //(g) optional
private int sodium = 0; //(mg) optional
private int carbohydrate = 0; //(g) optional

public NutritionFacts() {
}

public void setServingSize(int servingSize) {
this.servingSize = servingSize;
}

public void setServings(int servings) {
this.servings = servings;
}

public void setCalories(int calories) {
this.calories = calories;
}

public void setFat(int fat) {
this.fat = fat;
}

public void setSodium(int sodium) {
this.sodium = sodium;
}

public void setCarbohydrate(int carbohydrate) {
this.carbohydrate = carbohydrate;
}
}

// 实例化
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);

JavaBeans模式创建实例很容易,代码可读性也很强,但其有很严重的缺点:

  1. 构造的过程包含多个调用,在构造过程中JavaBeans可能处于不一致状态

    例如一个线程正在使用setter初始化值,而另一个线程正用getter取得该对象的字段

  2. 使用JavaBeans模式则该类不可成为不可变类 (因为有setter访问器)

3. Builder模式

此模式有重叠构造器的安全性,也有JavaBeans模式的高可读性

不直接生成需要的对象,而是得到一个builder对象,调用所有的必要的构造器或静态工厂,设置每一个需要设置的参数,最后调用一个无参数的build方法来生成一个不可变对象。

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
56
57
58
59
60
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 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 {
// Required parameters
private int servingSize;
private int servings;
private int calories;
private int fat;
private int sodium;
private int carbohydrate;

public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}

// builder的 setter 返回自身实现链接调用
public Builder calories(int calories) {
this.calories = calories;
return this;
}

public Builder fat(int fat) {
this.fat = fat;
return this;
}

public Builder sodium(int sodium) {
this.sodium = sodium;
return this;
}

public Builder carbohydrate(int carbohydrate) {
this.carbohydrate = carbohydrate;
return this;
}

public NutritionFacts build() {
return new NutritionFacts(this);
}
}
}

// 实例化
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).carbohydrate(27).build();

Builder 模式的可读性提高了很多,代码简易。Builder模式模拟了了具名可选参数

如果该类包含多个字段,Builder模式就是种不错的选择,特别是当大多数参数都是可选的时候。与重叠构造器与JavaBeans相比要更加安全与易读。