目录
本文是这个系列的第三篇文章,介绍了通过Builder模式应对参数过多的问题。如果你也希望参与类似的系列文章翻译,可以加入我们的Java开发 和 技术翻译 小组。
在前两篇文章中,我分别使用了自定义类型和参数对象方法来减少构造器或方法调用传入的参数数量。本文关注的是Builder模式。我认为它不仅能够帮助构造函数“瘦身”,甚至可以对非构造函数也能发挥同样的作用。
在Effective Java第二版中,Josh Bloch在第二章中就提到使用Builder模式处理需要很多参数的构造函数。他不仅展示了Builder的使用,也描述了相这种方法相对使用带很多参数的构造函数带来的好处。在本文的结尾我将进一步总结Builde模式的优点。需要指出的是Josh Bloch已经在他的书本贯穿了这一思想。
为了说明这种方法的优点,我会继续使用Person类。需要指出的是,为了更好地聚焦于类构造本身我只添加了个别方法。
Person.java (非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
|
package
dustin.examples;
/**
* Person class used as part of too many parameters demonstration.
*
* @author Dustin
*/
public
class
Person
{
private
final
String lastName;
private
final
String firstName;
private
final
String middleName;
private
final
String salutation;
private
final
String suffix;
private
final
String streetAddress;
private
final
String city;
private
final
String state;
private
final
boolean
isFemale;
private
final
boolean
isEmployed;
private
final
boolean
isHomewOwner;
public
Person(
final
String newLastName,
final
String newFirstName,
final
String newMiddleName,
final
String newSalutation,
final
String newSuffix,
final
String newStreetAddress,
final
String newCity,
final
String newState,
final
boolean
newIsFemale,
final
boolean
newIsEmployed,
final
boolean
newIsHomeOwner)
{
this
.lastName = newLastName;
this
.firstName = newFirstName;
this
.middleName = newMiddleName;
this
.salutation = newSalutation;
this
.suffix = newSuffix;
this
.streetAddress = newStreetAddress;
this
.city = newCity;
this
.state = newState;
this
.isFemale = newIsFemale;
this
.isEmployed = newIsEmployed;
this
.isHomewOwner = newIsHomeOwner;
}
}
|
这样写的类构造器也无可厚非,但当有许多参数时编写客户端代码会很困难。这时,如果使用Builder模式会有效提高代码的可读性。在我的博客中曾写过使用NetBeans能方便地实现代码重构。下面就是使用NetBeans重构过的代码。
PersonBuilder.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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
|
package
dustin.examples;
public
class
PersonBuilder
{
private
String newLastName;
private
String newFirstName;
private
String newMiddleName;
private
String newSalutation;
private
String newSuffix;
private
String newStreetAddress;
private
String newCity;
private
String newState;
private
boolean
newIsFemale;
private
boolean
newIsEmployed;
private
boolean
newIsHomeOwner;
public
PersonBuilder()
{
}
public
PersonBuilder setNewLastName(String newLastName) {
this
.newLastName = newLastName;
return
this
;
}
public
PersonBuilder setNewFirstName(String newFirstName) {
this
.newFirstName = newFirstName;
return
this
;
}
public
PersonBuilder setNewMiddleName(String newMiddleName) {
this
.newMiddleName = newMiddleName;
return
this
;
}
public
PersonBuilder setNewSalutation(String newSalutation) {
this
.newSalutation = newSalutation;
return
this
;
}
public
PersonBuilder setNewSuffix(String newSuffix) {
this
.newSuffix = newSuffix;
return
this
;
}
public
PersonBuilder setNewStreetAddress(String newStreetAddress) {
this
.newStreetAddress = newStreetAddress;
return
this
;
}
public
PersonBuilder setNewCity(String newCity) {
this
.newCity = newCity;
return
this
;
}
public
PersonBuilder setNewState(String newState) {
this
.newState = newState;
return
this
;
}
public
PersonBuilder setNewIsFemale(
boolean
newIsFemale) {
this
.newIsFemale = newIsFemale;
return
this
;
}
public
PersonBuilder setNewIsEmployed(
boolean
newIsEmployed) {
this
.newIsEmployed = newIsEmployed;
return
this
;
}
public
PersonBuilder setNewIsHomeOwner(
boolean
newIsHomeOwner) {
this
.newIsHomeOwner = newIsHomeOwner;
return
this
;
}
public
Person createPerson() {
return
new
Person(newLastName, newFirstName, newMiddleName, newSalutation, newSuffix, newStreetAddress, newCity, newState, newIsFemale, newIsEmployed, newIsHomeOwner);
}
}
|
相对于之前的写法,我更趋向于将builder作为它构建类的静态成员类(嵌套类)。即便是NetBeans自动生成的Builder使用起来也很方便。另一个区别在于,我希望Builder实现一个包含了必要字段的构造函数,而不是一个无参构造函数。
下面显示的是改进后的代码。
Person.java(Person.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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
|
package
dustin.examples;
/**
* Person class used as part of too many parameters demonstration.
*
* @author Dustin
*/
public
class
Person
{
private
final
String lastName;
private
final
String firstName;
private
final
String middleName;
private
final
String salutation;
private
final
String suffix;
private
final
String streetAddress;
private
final
String city;
private
final
String state;
private
final
boolean
isFemale;
private
final
boolean
isEmployed;
private
final
boolean
isHomewOwner;
public
Person(
final
String newLastName,
final
String newFirstName,
final
String newMiddleName,
final
String newSalutation,
final
String newSuffix,
final
String newStreetAddress,
final
String newCity,
final
String newState,
final
boolean
newIsFemale,
final
boolean
newIsEmployed,
final
boolean
newIsHomeOwner)
{
this
.lastName = newLastName;
this
.firstName = newFirstName;
this
.middleName = newMiddleName;
this
.salutation = newSalutation;
this
.suffix = newSuffix;
this
.streetAddress = newStreetAddress;
this
.city = newCity;
this
.state = newState;
this
.isFemale = newIsFemale;
this
.isEmployed = newIsEmployed;
this
.isHomewOwner = newIsHomeOwner;
}
public
static
class
PersonBuilder
{
private
String nestedLastName;
private
String nestedFirstName;
private
String nestedMiddleName;
private
String nestedSalutation;
private
String nestedSuffix;
private
String nestedStreetAddress;
private
String nestedCity;
private
String nestedState;
private
boolean
nestedIsFemale;
private
boolean
nestedIsEmployed;
private
boolean
nestedIsHomeOwner;
public
PersonBuilder(
final
String newFirstName,
final
String newCity,
final
String newState)
{
this
.nestedFirstName = newFirstName;
this
.nestedCity = newCity;
this
.nestedState = newState;
}
public
PersonBuilder lastName(String newLastName)
{
this
.nestedLastName = newLastName;
return
this
;
}
public
PersonBuilder firstName(String newFirstName)
{
this
.nestedFirstName = newFirstName;
return
this
;
}
public
PersonBuilder middleName(String newMiddleName)
{
this
.nestedMiddleName = newMiddleName;
return
this
;
}
public
PersonBuilder salutation(String newSalutation)
{
this
.nestedSalutation = newSalutation;
return
this
;
}
public
PersonBuilder suffix(String newSuffix)
{
this
.nestedSuffix = newSuffix;
return
this
;
}
public
PersonBuilder streetAddress(String newStreetAddress)
{
this
.nestedStreetAddress = newStreetAddress;
return
this
;
}
public
PersonBuilder city(String newCity)
{
this
.nestedCity = newCity;
return
this
;
}
public
PersonBuilder state(String newState)
{
this
.nestedState = newState;
return
this
;
}
public
PersonBuilder isFemale(
boolean
newIsFemale)
{
this
.nestedIsFemale = newIsFemale;
return
this
;
}
public
PersonBuilder isEmployed(
boolean
newIsEmployed)
{
this
.nestedIsEmployed = newIsEmployed;
return
this
;
}
public
PersonBuilder isHomeOwner(
boolean
newIsHomeOwner)
{
this
.nestedIsHomeOwner = newIsHomeOwner;
return
this
;
}
public
Person createPerson()
{
return
new
Person(
nestedLastName, nestedFirstName, nestedMiddleName,
nestedSalutation, nestedSuffix,
nestedStreetAddress, nestedCity, nestedState,
nestedIsFemale, nestedIsEmployed, nestedIsHomeOwner);
}
}
}
|
使用Builder模式时,如果结合我前两篇文章介绍的自定义类型和参数对象方法一起使用效果会更好。示例代码如下。
Person.java (嵌套类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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
|
package
dustin.examples;
/**
* Person class used as part of too many parameters demonstration.
*
* @author Dustin
*/
public
class
Person
{
private
final
FullName name;
private
final
Address address;
private
final
Gender gender;
private
final
EmploymentStatus employment;
private
final
HomeownerStatus homeOwnerStatus;
/**
* Parameterized constructor can be private because only my internal builder
* needs to call me to provide an instance to clients.
*
* @param newName Name of this person.
* @param newAddress Address of this person.
* @param newGender Gender of this person.
* @param newEmployment Employment status of this person.
* @param newHomeOwner Home ownership status of this person.
*/
private
Person(
final
FullName newName,
final
Address newAddress,
final
Gender newGender,
final
EmploymentStatus newEmployment,
final
HomeownerStatus newHomeOwner)
{
this
.name = newName;
this
.address = newAddress;
this
.gender = newGender;
this
.employment = newEmployment;
this
.homeOwnerStatus = newHomeOwner;
}
public
FullName getName()
{
return
this
.name;
}
public
Address getAddress()
{
return
this
.address;
}
public
Gender getGender()
{
return
this
.gender;
}
public
EmploymentStatus getEmployment()
{
return
this
.employment;
}
public
HomeownerStatus getHomeOwnerStatus()
{
return
this
.homeOwnerStatus;
}
/**
* Builder class as outlined in the Second Edition of Joshua Bloch's
* <em><span class="wp_keywordlink"><a href="http://www.amazon.com/gp/product/B000WJOUPA/ref=as_li_qf_sp_asin_il_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=B000WJOUPA&linkCode=as2&tag=job0ae-20" title="Effective Java" rel="nofollow" target="_blank" class="external">Effective Java</a></span></em> that is used to build a {@link Person} instance.
*/
public
static
class
PersonBuilder
{
private
FullName nestedName;
private
Address nestedAddress;
private
Gender nestedGender;
private
EmploymentStatus nestedEmploymentStatus;
private
HomeownerStatus nestedHomeOwnerStatus;
public
PersonBuilder(
final
FullName newFullName,
final
Address newAddress)
{
this
.nestedName = newFullName;
this
.nestedAddress = newAddress;
}
public
PersonBuilder name(
final
FullName newName)
{
this
.nestedName = newName;
return
this
;
}
public
PersonBuilder address(
final
Address newAddress)
{
this
.nestedAddress = newAddress;
return
this
;
}
public
PersonBuilder gender(
final
Gender newGender)
{
this
.nestedGender = newGender;
return
this
;
}
public
PersonBuilder employment(
final
EmploymentStatus newEmploymentStatus)
{
this
.nestedEmploymentStatus = newEmploymentStatus;
return
this
;
}
public
PersonBuilder homeOwner(
final
HomeownerStatus newHomeOwnerStatus)
{
this
.nestedHomeOwnerStatus = newHomeOwnerStatus;
return
this
;
}
public
Person createPerson()
{
return
new
Person(
nestedName, nestedAddress, nestedGender,
nestedEmploymentStatus, nestedHomeOwnerStatus);
}
}
}
|
最后2段代码列表展示了Builder构建对象的常见方法。实际上,《在Effective Java 第二版》第2条中就是用Builder来创建和销毁对象。此外,Builder能更简单地通过参数对象传递间接实现了一个非构造函数方法。在上面最后一段代码中,这些方法都接收了一些参数对象。诚然,即使使用了Builder构建参数对象还是比较枯燥的。但每一次Builder构建都是间接让非构造函数方法受益,因为参数对象使用确实有效减少了方法参数的数量。
下面重新定义的FullName和Address类既能做参数本身也使用了Builder模式。
FullName.java(使用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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
|
package
dustin.examples;
/**
* Full name of a person.
*
* @author Dustin
*/
public
final
class
FullName
{
private
final
Name lastName;
private
final
Name firstName;
private
final
Name middleName;
private
final
Salutation salutation;
private
final
Suffix suffix;
private
FullName(
final
Name newLastName,
final
Name newFirstName,
final
Name newMiddleName,
final
Salutation newSalutation,
final
Suffix newSuffix)
{
this
.lastName = newLastName;
this
.firstName = newFirstName;
this
.middleName = newMiddleName;
this
.salutation = newSalutation;
this
.suffix = newSuffix;
}
public
Name getLastName()
{
return
this
.lastName;
}
public
Name getFirstName()
{
return
this
.firstName;
}
public
Name getMiddleName()
{
return
this
.middleName;
}
public
Salutation getSalutation()
{
return
this
.salutation;
}
public
Suffix getSuffix()
{
return
this
.suffix;
}
@Override
public
String toString()
{
return
this
.salutation +
" "
+
this
.firstName +
" "
+
this
.middleName
+
this
.lastName +
", "
+
this
.suffix;
}
public
static
class
FullNameBuilder
{
private
final
Name nestedLastName;
private
final
Name nestedFirstName;
private
Name nestedMiddleName;
private
Salutation nestedSalutation;
private
Suffix nestedSuffix;
public
FullNameBuilder(
final
Name newLastName,
final
Name newFirstName)
{
this
.nestedLastName = newLastName;
this
.nestedFirstName = newFirstName;
}
public
FullNameBuilder middleName(
final
Name newMiddleName)
{
this
.nestedMiddleName = newMiddleName;
return
this
;
}
public
FullNameBuilder salutation(
final
Salutation newSalutation)
{
this
.nestedSalutation = newSalutation;
return
this
;
}
public
FullNameBuilder suffix(
final
Suffix newSuffix)
{
this
.nestedSuffix = newSuffix;
return
this
;
}
public
FullName createFullName()
{
return
new
FullName(
nestedLastName, nestedFirstName, nestedMiddleName,
nestedSalutation, nestedSuffix);
}
}
}
|
Address.java (使用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
62
63
64
65
|
package
dustin.examples;
/**
* Representation of a United States address.
*
* @author Dustin
*/
public
final
class
Address
{
private
final
StreetAddress streetAddress;
private
final
City city;
private
final
State state;
private
Address(
final
StreetAddress newStreetAddress,
final
City newCity,
final
State newState)
{
this
.streetAddress = newStreetAddress;
this
.city = newCity;
this
.state = newState;
}
public
StreetAddress getStreetAddress()
{
return
this
.streetAddress;
}
public
City getCity()
{
return
this
.city;
}
public
State getState()
{
return
this
.state;
}
@Override
public
String toString()
{
return
this
.streetAddress +
", "
+
this
.city +
", "
+
this
.state;
}
public
static
class
AddressBuilder
{
private
StreetAddress nestedStreetAddress;
private
final
City nestedCity;
private
final
State nestedState;
public
AddressBuilder(
final
City newCity,
final
State newState)
{
this
.nestedCity = newCity;
this
.nestedState = newState;
}
public
AddressBuilder streetAddress(
final
StreetAddress newStreetAddress)
{
this
.nestedStreetAddress = newStreetAddress;
return
this
;
}
public
Address createAddress()
{
return
new
Address(nestedStreetAddress, nestedCity, nestedState);
}
}
}
|
由于用了Builder,新的Person实例可以通过下面这种方式创建。当然为了比较,也给出了传统的Person实例化方法。
1
2
3
4
5
6
7
8
9
10
11
12
|
final
Person person1 =
new
Person.PersonBuilder(
new
FullName.FullNameBuilder(
new
Name(
"Dynamite"
),
new
Name(
"Napoleon"
)).createFullName(),
new
Address.AddressBuilder(
new
City(
"Preston"
), State.ID).createAddress()).createPerson();
final
Person person2 =
new
Person.PersonBuilder(
new
FullName.FullNameBuilder(
new
Name(
"Coltrane"
),
new
Name(
"Rosco"
)).middleName(
new
Name(
"Purvis"
)).createFullName(),
new
Address.AddressBuilder(
new
City(
"Hazzard"
), State.GA).createAddress())
.gender(Gender.MALE).employment(EmploymentStatus.EMPLOYED).createPerson();
|
实例化Person(不使用Builder)
final person = new Person(“Coltrane”, “Rosco”, “Purvis”, null, “Hazzard”, “Georgia”, false, true, true);
看完代码高下立判,调用传统JAVA构造器的客户端代码的可读性远不如使用了Builder的客户端代码。同一类型的变量和空放置在一起被调用将会导致一些微妙的错误。(试想,如果客户端不小心颠倒了其中的几个参数顺序,编译不会出错但在运行时肯定出错)
Builder模式好处和优点
使用Builder模式必然会导致写两遍相关属性的代码和SETTER方法,看起来有点吃力不讨好。然而需要看到的是,客户端代码的可用性和可读性得到了大大提高。与此同时,构造函数的参数数量明显减少调用起来非常直观。
Builder方法另外一个优势在于,单个builder构建多个对象时Builder参数可在创建期间进行调整,还可以根据对象不同而进行改变。这就像我越来越推崇的以“不变”应“万变”。Builder模式特别适合那些属性个数很多的类,我认为没有必要给那些本不需要设置值传递参数(设置null)。
Builder模式在提高代码可读性的同时,使用IDE提供的代码补全功能也更加容易。Builder模式在与构造函数一起使用带来的更大优势在Josh Bloch的Effective Java第二版第2条中有详细阐述。
Builder模式的代价和缺点
使用Builder模式是肯定会增加代码量的。此外,尽管客户端的代码可读性明显改善,但随之而来的客户端代码变得更加冗长。我还是坚持这一观点:相比较参数数量的增加,相同类型的参数混在一起,可选参数的增加而言,改善代码可读性更有价值。
Builder会增加个类代码,这也意味着开发者在给类增加属性时有时会忘记给该属性添加支持的builder。为了克服这个问题,通常我会将builder嵌套到类中,这样可以很容易地发现哪个相关的builder需要更新。尽管忘记的风险依旧存在,但是这风险就像忘记给类的新属性增加 toString()、 equals(Object)、 hashCode()或其他类基于是所有属性的方法一样。
在我的Builder实现中,我会用Builder的构造函数而不是set方法传递客户需要的属性。这样做的好处在于,对象总是能被一次完整的实例化,而不是靠开发人员调用时用set方法补充额外的属性完成实例化。这也体现了不可变性带来的好处。然而,相应地也会造成自己设定的属性方法可读性降低。
正如它名字表示的那样,Builder只是一个替代构造器的选择不能直接用于降低非构造函数方法的参数数量,但是结合参数对象的使用就能达到这一点。有关更多反对用Builder来进行对象构建的讨论可以参见 A dive into the Builder pattern上的相关评论。
总结
构建对象时,如果碰到类有很多参数——其中很多参数类型相同而且很多参数可以为空时,我更喜欢Builder模式来完成。当参数数量不多、类型不同而且都是必须出现时,通过增加代码实现Builder往往无法体现它的优势。在这种情况下,理想的方法是调用传统的构造函数。再者,如果不需要保持不变,那么就使用无参构造函数调用相应的set方法吧。