建造者模式的使用

因为一个新项目的原因,去北京呆了半个多月,所以有好长时间没有写博客了。这段时间在写代码的过程中,学习到了建造者模式的一种用法,记录下来(设计模式在平时也会用到,网上文章多如牛毛,所以没怎么写关于设计模式这方面的日志)

建造者模式

建造者模式用来创建一个内部结构复杂的对象,拥有多个组成部分。建造者模式可以使用户无需知道这些组件的装配细节,只需要指定复杂对象的类型就可以得到该对象。

有的复杂对象的组装还可能有一些限制条件,比如,某些属性必须存在,某些属性的初始化需要遵从一定的顺序等等。举一个修两层楼的比方:必须打好地基才能盖第一层楼,第一层盖好后才能盖第二层。

复杂对象的创建过程被封装到一个建造者对象里面,用户可以直接使用建造者建造完毕的对象,而不必关心建造过程。

builder

建造者模式包含上面几个对象:

Builder:抽象建造者

ConcreteBuilder:具体建造者

Director:指挥者

Product:产品角色

还是用修楼那个例子来分析:

Client:

1
2
3
4
5
6
7
8
9
10
11
package space.kyu.mode.builder;
public class Client {
public static void main(String[] args) {
Builder builder = new OneBuilder();
Director director = new Director();
director.build(builder);
Building building = builder.getResult();
System.out.println(building);
}
}

Director:

1
2
3
4
5
6
7
8
9
package space.kyu.mode.builder;
public class Director {
public void build(Builder builder) {
builder.buildFoundation();
builder.buildLayer();
builder.buildSecondLayer();
}
}

Building:

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
package space.kyu.mode.builder;
public class Building {
private int foundation;//地基深度
private int layer;//一层高度
private int secondLayer;//二层高度
public int getFoundation() {
return foundation;
}
public void setFoundation(int foundation) {
this.foundation = foundation;
}
public int getLayer() {
return layer;
}
public void setLayer(int layer) {
this.layer = layer;
}
public int getSecondLayer() {
return secondLayer;
}
public void setSecondLayer(int secondLayer) {
this.secondLayer = secondLayer;
}
@Override
public String toString() {
return "Building [foundation=" + foundation + ", layer=" + layer
+ ", secondLayer=" + secondLayer + "]";
}
}

Builder:

1
2
3
4
5
6
7
8
package space.kyu.mode.builder;
interface Builder {
void buildFoundation();
void buildLayer();
void buildSecondLayer();
Building getResult();
}

OneBuilder:

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
package space.kyu.mode.builder;
public class OneBuilder implements Builder {
Building building = new Building();
@Override
public void buildFoundation() {
building.setFoundation(5);对象
}
@Override
public void buildLayer() {
building.setLayer(3);
}
@Override
public void buildSecondLayer() {
building.setSecondLayer(3);
}
@Override
public Building getResult() {
return building;
}
}

TwoBuilder:

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
package space.kyu.mode.builder;
public class TwoBuilder implements Builder {
Building building = new Building();
@Override
public void buildFoundation() {
building.setFoundation(4);
}
@Override
public void buildLayer() {
building.setLayer(2);
}
@Override
public void buildSecondLayer() {
building.setSecondLayer(2);
}
@Override
public Building getResult() {
return building;
}
}

上面的例子中:

类Client就是用户

类Director就是指挥者Director

类Builder就是抽象建造者Builder

类Building就是产品Product

类OneBuilder、TwoBuilder就是实际建造者

可以看到,我们在客户端中只需要指定实际建造者,指挥类就可以为我们生成想要的产品。我们完全不需要知道这个二层小楼的建造过程,使用者与建造过程顺利解藕。并且,如果需要不同设计方案的二层小楼,我们只需要再实现一个实际建造者,并在客户端中指定即可。而且,修改一个实际建造者不会影响到其他的实现。

建造者模式与工厂方法模式很类似,如果把上面的指挥类当作客户端的话,那么他基本上就是一个工厂方法模式了。

建造者模式 与工厂方法模式的区别就在于增加了这个指挥类,因此建造者模式适用于创建过程更加复杂的对象。

扩展

其实这个扩展才是这篇日志的重点,是建造者模式的一种延伸,也是我在这段时间里面学习到的一种用法。

这个用法一开始是在使用org.apache.http.client.utils.URIBuilder的时候学到的,当时用到的时候就觉得眼前一亮,马上想到了建造者模式的Builder,而且使用了流式接口,用起来很顺畅的感觉…

后来看到了网上的一篇文章设计模式(十)——建造者模式的实践讲的就是这个用法,也是翻译了老外的一篇博客。原文已经分析的很好了,所以直接转载过来了。(作者的其他博客也是很通俗易懂的,值的学习)

以下内容引用自设计模式(十)——建造者模式的实践


我不打算深入介绍设计模式的细节内容,因为有很多这方面的文章和书籍可供参考。

本文主要关注于告诉你为什么以及在什么情况下你应该考虑使用建造者模式。

然而,值得一提的是本文中的模式和GOF中的提出的有点不一样。那种原生的模式主要侧重于抽象构造的过程以达到通过修改builder的实现来得到不同的结果的目的。本文中主要介绍的这种模式并没有那么复杂,因为我删除了不必要的多个构造函数、多个可选参数以及大量的setter/getter方法。

假设你有一个类,其中包含大量属性。就像下面的User类一样。假设你想让这个类是不可变的。

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
...
}

在这样的类中,有一些属性是必须的(required)而另外一些是可选的(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;
}

首先可以肯定的是,这样做是可以满足要求的。

当然,这种方式的缺点也是很明显的。当一个类中只有几个参数的时候还好,如果一旦类中的参数逐渐增大,那么这个类就会变得很难阅读和维护。更重要的是,这样的一个类,调用者会很难使用。我到底应该使用哪个构造方法?是包含两个参数的还是包含三个参数的?如果我没有传递值的话那些属性的默认值是什么?如果我只想对address赋值而不对age和phone赋值怎么办?遇到这种情况可能我只能调用那个参数最全的构造函数,然后对于我不想要的参数值传递一个默认值。

此外,如果多个参数的类型都相同那就很容易让人困惑,第一个String类型的参数到底是number还是address呢?

还有没有其他方案可选择呢?我们可以遵循JaveBean规范,定义一个只包含无参数的构造方法和getter、setter方法的JavaBean。

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;
}
}

这种方式看上去很容易阅读和维护。对于调用者来说,我只需要创建一个空的对象,然后对于我想设置的参数调用setter方法设置就好了。这难道还有什么问题吗?其实存在两个问题。第一个问题是该类的实例状态不固定。如果你想创建一个User对象,该对象的5个属性都要赋值,那么直到所有的setXX方法都被调用之前,该对象都没有一个完整的状态。这意味着在该对象状态还不完整的时候,一部分客户端程序可能看见这个对象并且以为该对象已经构造完成。

这种方法的第二个不足是User类是易变的(因为没有属性是final的)。你将会失去不可变对象带来的所有优点。

幸运的是应对这种场景我们有第三种选择,建造者模式。解决方案类似如下所示:

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方法。

builder类使用流式接口风格,让客户端代码阅读起来更容易(我们马上就会看到一个它的例子)

builder的构造方法只接收必要的参数,为了确保这些属性在构造方法里赋值,只有这些属性被定义成final类型。

使用建造者模式有在本文开始时提到的两种方法的所有优点,并且没有它们的缺点。客户端代码写起来更简单,更重要的是,更易读。我听过的关于该模式的唯一批判是你必须在builder类里面复制类的属性。然而,考虑到这个事实,builder类通常是需要建造的类的一个静态类成员,它们一起扩展起来相当容易。(译者表示没明白为设定为静态成员扩展起来就容易了。设为静态成员我认为有一个好处就是可以避免出现is not an enclosing class的编译问题,创建对象时候更加方便)

现在,试图创建一个新的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();
}

译者注:如果UserBuilder没有设置为static的,以上代码会有编译错误。错误提示:User is not an enclosing class

以上代码看上去相当整洁。我们可以只通过一行代码就可以创建一个User对象,并且这行代码也很容易读懂。除此之外,这样还能确保无论何时你想获取该类的对象都不会是不完整的(译者注:因为创建对象的过程是一气呵成的,一旦对象创建完成之后就不可修改了)。

这种模式非常灵活,一个单独的builder类可以通过在调用build方法之前改变builder的属性来创建多个对象。builder类甚至可以在每次调用之间自动补全一些生成的字段,例如一个id或者序列号。

值得注意的是,像构造函数一样,builder可以对参数的合法性进行检查,一旦发现参数不合法可以抛出IllegalStateException异常。

但是,很重要的一点是,如果要检查参数的合法性,一定要先把参数传递给对象,然后在检查对象中的参数是否合法。其原因是因为builder并不是线程安全的。如果我们在创建真正的对象之前验证参数,参数值可能被另一个线程在参数验证完和参数被拷贝完成之间的时间修改。这段时间周期被称作“脆弱之窗”。我们的例子中情况如下:

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);
}

建造者模式最后的一个优点是builder可以作为参数传递给一个方法,让该方法有为客户端创建一个或者多个对象的能力,而不需要知道创建对象的任何细节。为了这么做你可能通常需要一个如下所示的简单接口:

1
2
3
public interface Builder {
T build();
}

借用之前的User例子,UserBuilder类可以实现Builder。如此,我们可以有如下的代码:

UserCollection buildUserCollection(Builder userBuilder){…}

译者注:关于这这最后一个优点的部分内容并没太看懂,希望有理解的人能过不吝赐教。