原文链接:http://www.importnew.com/14849.html
这是我第一篇文章(也是我关于这个主题的第一篇博客)。我记不清在哪读过这项内容(尽管我基本上确认是在Practices of an Agile Developer上看到的),但是写博客应该能帮助你全神贯注。具体点来说,通过花些时间来解释你所知道的东西,你能更好的理解它。
这也正是我想要努力去做的,通过解释一件事,继而进一步理解这件事。并且还有个额外的好处,当我回忆曾经做过的事情时,它是一个很好的集中地。希望在这过程中也能帮助到你们。
废话不多,让我们直奔主题——构造模式。我不打算分割成许多细节来讲,因为已经有非常多的稿件,书籍详细的说明过这个模式。 反而,我会告诉你为什么,以及什么时候应该考虑使用它。然而,值得一提的是,这里所说的模式和四人帮书中的模式有些不同。尽管原始的模式聚焦于抽象出构建的步骤,这样通过改变建造者的实现就可以得到不同的结果,本篇所说的模式着眼于从多构造器,多可选参数以及过度使用的setter中移除不必要的复杂性。
想象下你有一个类,像下图所示有许多属性。假设你想让你的类不可变(顺便说一下,除非有一个好的理由不这样做,否则你应该坚持。但是我们会以另一种方式来达到要求。)
1
2
3
4
5
6
7
8
|
public
class
User {
private
final
String firstName;
//required
private
final
String lastName;
//required
private
final
int
age;
//optional
private
final
String phone;
//optional
private
final
String address;
//optional
...
}
|
现在,想象下你的类中有些属性是必须的,有些则是可选的。你将要如何创建你的对象?所有的属性都声明为final,所以你必须在构造器中给它们全部赋值,但是你也想给这个类的客户端忽略可选属性的机会。
第一个可行的选择是拥有一个只接受必要属性作为参数的构造器,还要一个构造器接受所有的必要属性以及第一个可选属性,再有一个构造器接受两个可选属性等等。它是什么样子呢?像下面这个样子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public
User(String firstName, String lastName) {
this
(firstName, lastName,
0
);
}
public
User(String firstName, String lastName,
int
age) {
this
(firstName, lastName, age,
""
);
}
public
User(String firstName, String lastName,
int
age, String phone) {
this
(firstName, lastName, age, phone,
""
);
}
public
User(String firstName, String lastName,
int
age, String phone, String address) {
this
.firstName = firstName;
this
.lastName = lastName;
this
.age = age;
this
.phone = phone;
this
.address = address;
}
|
这种方式来构建类的实例的好处是它能很好的工作。然而,这种方式的问题也很明显。当你只有几个属性时还好,但是当这个数字扩大时,代码就变的难以理解和维护了。
更重要的是,代码对客户端来说变的很难。客户端应该调用哪个构造器?有两个参数的?有三个参数的?那些不用传确切值的参数的默认值是多少?如果我想给地址赋值,但是不给age和phone赋值要怎么办?那种情况下,我就不得不调用接受所有参数的构造器,并且给那些不需要的传入不在乎的默认值。此外,几个类型相同的参数是很令人费解的。第一个String是电话还是地址? 那么在这些情况下,我们还有其他选择吗?我们可以依照JavaBeans的约定,一个无参构造并且每个参数提供一个get和set。类似下面这个:
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
|
public
class
User {
private
String firstName;
// required
private
String lastName;
// required
private
int
age;
// optional
private
String phone;
// optional
private
String address;
//optional
public
String getFirstName() {
return
firstName;
}
public
void
setFirstName(String firstName) {
this
.firstName = firstName;
}
public
String getLastName() {
return
lastName;
}
public
void
setLastName(String lastName) {
this
.lastName = lastName;
}
public
int
getAge() {
return
age;
}
public
void
setAge(
int
age) {
this
.age = age;
}
public
String getPhone() {
return
phone;
}
public
void
setPhone(String phone) {
this
.phone = phone;
}
public
String getAddress() {
return
address;
}
public
void
setAddress(String address) {
this
.address = address;
}
}
|
这 种方式看起来容易理解和维护。作为客户端,我只需要创建一个空对象并且set我感兴趣的属性即可。那么这种方式有什么弊端呢?有两个主要弊端。第一个是类 实例的不一致状态。如果你要用User的五个属性来创建一个User对象,那么在所有的setX方法调用前,对象处于不完全状态。这就意味着客户端的其他 部分可能看到对象,并且假设它已经完成构造了,实际它并没有。方法的第二个缺点是对象可变。你丧失了不可变对象的所有好处。
幸运的是还有第三个选择,建造者模式,方案看起来是下面这样的。
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
|
public
class
User {
private
final
String firstName;
// required
private
final
String lastName;
// required
private
final
int
age;
// optional
private
final
String phone;
// optional
private
final
String address;
// optional
private
User(UserBuilder builder) {
this
.firstName = builder.firstName;
this
.lastName = builder.lastName;
this
.age = builder.age;
this
.phone = builder.phone;
this
.address = builder.address;
}
public
String getFirstName() {
return
firstName;
}
public
String getLastName() {
return
lastName;
}
public
int
getAge() {
return
age;
}
public
String getPhone() {
return
phone;
}
public
String getAddress() {
return
address;
}
public
static
class
UserBuilder {
private
final
String firstName;
private
final
String lastName;
private
int
age;
private
String phone;
private
String address;
public
UserBuilder(String firstName, String lastName) {
this
.firstName = firstName;
this
.lastName = lastName;
}
public
UserBuilder age(
int
age) {
this
.age = age;
return
this
;
}
public
UserBuilder phone(String phone) {
this
.phone = phone;
return
this
;
}
public
UserBuilder address(String address) {
this
.address = address;
return
this
;
}
public
User build() {
return
new
User(
this
);
}
}
}
|
有几个重点需要注意一下:
- User的构造器是私有的,这就意味着客户端不能直接创建实例。
- 这个类是不可变的。所有属性都是final类型并且他们由构造器设置值。此外,我们只提供getter操作。
- 建造者使用流式接口习语来让客户端代码更易读(下面会有示例)。
- 建造者的构造器只接受两个必须的参数,并且这两个属性是仅有的被设置为final类型的,这样就能保证这些属性在构造器中是被赋值的。
建造者模式的使用拥有开始所提两种方案的所有优点,并且没有它们的缺点。客户代码更容易写,最重要的是更易读。关于这个模式,我听到的唯一缺点是必须要复制类的属性到建造者中。既然建造者类通常是它所建造类的一个静态成员类,它们能相当容易的一起演进。
那么,客户代码尝试创建一个新的User对象会是什么样的?让我们来看看:
1
2
3
4
5
6
7
8
|
public
User getUser() {
return
new
User.UserBuilder(
"Jhon"
,
"Doe"
)
.age(
30
)
.phone(
"1234567"
)
.address(
"Fake address 1234"
)
.build();
}
|
很工整,不是吗?你能在一行内创建一个User对象,最重要的是它很容易理解。而且,你能确保,无论什么时候你拿到这个类的一个对象,它的状态都是完整的。
这个模式非常灵活。一个建造者可以通过在多次调用“build”之间改变属性用来创建多个对象。构造者甚至可以在两次调用之间自动补全一些生成的字段。例如id或其他序列号。
重点是,类似于构造器,建造者可以强制其参数的不变性。建造方法可以检查这些不变性, 如果它们无效就抛出IllegalStateException异常。关键是可以在从建造者中拷贝参数到对象时检查,并且是在对象字段上检查而不是在构造 器字段。这样做的理由是,既然建造者不是线程安全的,如果我们在实际创建对象前检查参数,参数值可能会在检查和拷贝之间被另一个线程改变。这个阶段的时间 被认为是“易损窗口”。在我们的例子中看起来是如下这样的:
1
2
3
4
5
6
7
|
public
User build() {
User user =
new
user(
this
);
if
(user.getAge() >
120
) {
throw
new
IllegalStateException(“Age out of range”);
// thread-safe
}
return
user;
}
|
之前的版本是线程安全的,因为我们先创建user然后检查不可变对象的不变性。下面的代码看起来功能一样,但是它不是线程安全的,你应该避免这样使用:
1
2
3
4
5
6
7
|
public
User build() {
if
(age >
120
) {
throw
new
IllegalStateException(“Age out of range”);
// bad, not thread-safe
}
// This is the window of opportunity for a second thread to modify the value of age
return
new
User(
this
);
}
|
最后一个优点是建造者可以被传入到一个方法中,来让这个方法为客户创建一个或多个对象,而不用知道任何对象创建的细节。你通常需要一个简单的接口来完成此功能:
1
2
3
|
public
interface
Builder {
T build();
}
|
在上面的例子中,UserBuilder类可以实现Builder接口。我们就可以使用下面这种方式:
1
|
UserCollection buildUserCollection(Builder<?
extends
User> userBuilder){...}
|
这真是个很长的首发文章。总结一下,建造者模式是处理超过一个参数的类的绝佳选择(这不是严格意义上的说法,但是我通常将接受四个属性的类当成使用这种模式的暗示),特别是如果大部分的参数是可选的。你的客户端代码会更易读,易写,易维护。此外,你的类可以保持不变,这点可以让你的代码更安全。
更新:如果你使用Eclipse作为你的IDE,有一些插件可以让你避免建造者中大部分的官样文章代码。下面这三个是比较推荐的:
- http://code.google.com/p/bpep/
- http://code.google.com/a/eclipselabs.org/p/bob-the-builder/
- http://code.google.com/p/fluent-builders-generator-eclipse-plugin/
我个人还没使用过其中任何一种插件,所以对于哪个更好,我没办法提供一个指导性的意见。我估计其他IDE应该也有类似的插件。