Java 构建器

静态工厂和构造器有个共同的局限性:它们都不能很好地扩展到大量的可选参数。考虑一个类的构造器具有多个参数时,就显得很不方便。

对于这样的类,应该用哪种构造器或者静态方法来编写呢?程序员一般会采用重叠构造器模式,在这种模式下,你提供一个只有必要参数的构造器,第二个构造器有一个可选参数,第三个有两个可选参数,依次类推,最后一个构造器包含所有的可选参数。下面我们来看下这种示例:

重叠构造器模式

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
public class Person {
private String name;
private int age;
private double height;
private double weight;
private String character;
private String hobby;

public Person(String name) {
this.name = name;
}

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public Person(String name, int age, double height) {
this.name = name;
this.age = age;
this.height = height;
}

public Person(String name, int age, double height, double weight) {
this.name = name;
this.age = age;
this.height = height;
this.weight = weight;
}

public Person(String name, int age, double height, double weight, String character) {
this.name = name;
this.age = age;
this.height = height;
this.weight = weight;
this.character = character;
}

public Person(String name, int age, double height, double weight, String character, String hobby) {
this.name = name;
this.age = age;
this.height = height;
this.weight = weight;
this.character = character;
this.hobby = hobby;
}
}

当我们想要创建实例的时候,就利用各个不同参数的构造器,但是假如我们需要实例化一个最长参数的构造器,它就会变成这样:

1
Person person = new Person("Bob", 19, 1.85, 76.5, "温和", "旅行");

这个构造器调用通常有许多你本不想设置的参数,但还是不得不为它们传递值。重叠构造器模式虽然在参数少的情况下可行,但随着参数数目的增加,我们通常很难去编写,并且也不易阅读。如果用户想知道传参值的含义,必须仔细地数着这些参数来探个究竟。如果用户不小心颠倒了其中两个参数的顺序,编译器可能不会出错,但是在程序运行的时候也会发生致命的错误。

当我们遇到这种情况时,我们还有第二种代替的办法,即JavaBeans模式。在这种模式下调用一个无参构造器来实例化对象,然后调用setter方法来设置每个所需要的参数:

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
public class Person {
private String name;
private int age;
private double height;
private double weight;
private String character;
private String hobby;

public Person() {}

public void setName(String name) {
this.name = name;
}

public void setAge(int age) {
this.age = age;
}

public void setHeight(double height) {
this.height = height;
}

public void setWeight(double weight) {
this.weight = weight;
}

public void setCharacter(String character) {
this.character = character;
}

public void setHobby(String hobby) {
this.hobby = hobby;
}
}

这种模式弥补了重叠构造器模式的不足。创建实例很容易,这样产生的代码读起来也很容易:

1
2
3
4
5
6
7
	Person person = new Person();
person.setName("Bob");
person.setAge(19);
person.setHeight(1.85);
person.setWeight(76.5);
person.setCharacter("温和");
person.setHobby("旅行");

遗憾的是,JavaBeans模式自身有着很严重的缺点。因为构造过程被分到了几个调用中,在构造过程中JavaBean可能处于不一致的状态。类无法仅仅通过检验构造器参数的有效性来保证一致性。试图使用处于不一致状态的对象,将会导致失败,这种失败与包含错误的代码大相径庭,因此调试起来十分困难。另外,JavaBeans模式阻止了把类做成不可变的可能,这就需要程序员付出额外的努力来确保它的线程安全。

所以,我们还有第三种替代方法,技能保证像重叠构造器那样的安全性,也能保证JavaBeans模式的可读性,这就是Builder模式。不直接生成想要的对象,而是让客户端在builer对象上调用类似于etter的方法,来设置每个相关的可选参数。最后,客户端调用无参的build方法来生成不可变的对象。这个builder是它构建的类的静态成员类。如下所示:

构建器模式

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
61
public class Person {
private final String name;
private final int age;
private final double height;
private final double weight;
private final String character;
private final String hobby;

public static Builder newBuilder(String name, int age) {
return new Builder(name, age);
}

public static class Builder implements BuilderFactory<Person> {
//必填参数
private final String name;
private final int age;
//可选参数
private double height;
private double weight;
private String character;
private String hobby;

public Builder(String name, int age) {
this.name = name;
this.age = age;
}

public Builder setHeight(double height) {
this.height = height;
return this;
}

public Builder setWeight(double weight) {
this.weight = weight;
return this;
}

public Builder setCharacter(String character) {
this.character = character;
return this;
}

public Builder setHobby(String hobby) {
this.hobby = hobby;
return this;
}

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

private Person(Builder builder) {
this.name = builder.name;
this.age = builder.age;
this.height = builder.height;
this.weight = builder.weight;
this.character = builder.character;
this.hobby = builder.hobby;
}
}

这里注意Person类是不可变的,所有的默认参数值都单独放在一个地方。buildersetter方法返回builder本身,以便可以把调用链接起来。下面看下调用代码:

1
2
3
4
5
6
Person person = Person.newBuilder("Bob", 19)
.setHeight(1.85)
.setWeight(76.5)
.setCharacter("温和")
.setHobby("旅行")
.build();

这样的客户端代码很容易编写和易于阅读,更为重要的是它也可以是安全的。uilder像个构造器一样,可以对其参数强加约束条件。build方法可以检验这些约束条件,将参数从builder拷贝到对象中之后,并在对象域而不是builder域中对它们进行检验,这一点很重要,。如果违反了任何的约束条件,build方法就应该抛出参数异常错误(IllegalStateException),异常的信息显示出违反了哪个约束条件。

对多个参数强加约束条件的另一种方法是,用多个setter方法对某个约束条件必须持有的所有参数进行检查。如果没有满足约束条件,setter方法就抛出IllegalArgumentException。这样一旦发现了错误,立即发现异常,而不是等着调用build方法。

设置了参数的builder生成了一个很好的抽象工厂。客户端可以将这样的一个builder传给方法,使该方法为客户端创建一个或多个对象。要使用这种方法,我们需要有个类型来表示builder,只需要一个泛型即可满足构建的所有对象的类型:

1
2
3
public interface BuilderFactory<T> {
T build();
}

接着我们就可以声明PersonBuilder类来实现BuilderFactory<Person>

1
2
3
4
5
6
public static class Builder implements BuilderFactory<Person> {
//...fields
public Person build() {
//todo
}
}

Builder模式的确也有自身不足的地方。为了创建对象,必须先创建它的构造器。虽然创建构建器的开销在实践中可能不那么明显,但是在十分注重性能的情况下可能就是一个问题了。Builder模式比重叠构造器模式更加冗长,因此它只有在很多参数的时候才会去使用,比如4个以上的参数。

简而言之,如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder模式就是种不错的选择。