Builder pattern
这里所介绍的建造者模式不是GOF中介绍的建造者模式。GOF中的建造者模式主要用于抽象构造的步骤,所以通过使用不同的建造者实现来获得不同的结果。
这里介绍的建造者模式是用于去除构造对象时不必要的复杂性,例如多构造器,多可选的参数和过度使用的Setter方法。
假设有一个User
类,你想要将它设置为不可变的,而其中有些属性是必需的,而有些是可选的。所有的属性都是final
的,所以只能在构造器中初始化。
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
...
}
在我们还不了解建造者模式的时候,我们可能会这么写
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;
}
这个方式当然是正确的,但是这个方式的缺点也很明显。当你仅仅只有一点点的属性时,问题并不大,但是当
属性的数量增多时,代码就变得很难阅读和维护。更为重要的是,客户端地调用越来越难,客户端并不知道在那么多的
构造器中要选择哪一个。而且过多的参数也会让调用的人感到困惑,甚至于传错参数。(!- - 这个问题我也经常遇到)。
当然,我们也可以是使用JavaBeans
约定,也就是有一个无参构造函数以及每个属性对应的Getter/Setter
。如下:
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来给我们需要的属性值设值。
但是这里有两个问题:
- 我们可能拥有一个状态不一致的实例。
- 该实例是可变的。
对于1来说,假设我们需要创建一个User
对象,并且设置它的五个属性。那么在调用所有的五个Setter
方法
来给属性设值之前,User
对象没有一个完整的状态。也就是说,在构造该对象的过程中,客户应用的某些部分
可能会认为该对象已经构造完成,但是实际上并没有。
对于2来说,我们就对丢失不可变对象的优势,因为通过JavaBeans
方式构造出来的对象都是可变的。
幸好我们还有第三种方式,通过建造者模式。
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
类的构造器是private
的,所以客户端代码没办法直接实例化。 - 该类是不可变的(
immutable
)。所有的属性都是final
的,都是通过构造器设置的。而且我们只提供了Getter
方法,而没有Setter
方法。 - 建造者使用了流式接口(
Fluent Interface
)形式来提高客户端代码的可读性。 - 建造者的构造器只要求必需的属性,而且被声明为
final
。
通过建造者,客户端代码变得容易书写,最重要的是容易阅读。对该模式唯一的批评是,你不得不在建造者中重复类的属性。
但是因为建造者通常都是它所建造的类的静态成员类,所以将它们合并起来相当简单。
现在我们来看看如何使用它。
public User getUser() {
return new
User.UserBuilder("Jhon", "Doe")
.age(30)
.phone("1234567")
.address("Fake address 1234")
.build();
}
容易书写容易阅读,而且不管何时你获得该类的一个对象都不可能处于不一致的状态。
建造者模式非常地灵活。一个建造者可以创建多个对象,仅仅只需要在两次调用build
方法之前更改建造者的属性。
很重要的一点是,就像构造器,建造者可以给它的参数强加不变性。build
方法可以检查不变量,如果他们不是有效的就抛出IllegalStateException
。在从建造者拷贝到对象之后再进行检查是很关键的,因为建造者是线程不安全的,当我们在真正地创建对象之前就检验参数,那么它们的值可能被另一个线程所改变。
public User build() {
User user = new user(this);
if (user.getAge() > 120) {
throw new IllegalStateException(“Age out of range”); // thread-safe
}
return user;
}
因为我们先创建出user
,然后再在不可变对象的基础上检验不变量,所以这个写法是线程安全的。
下面这个例子看起来功能相同,但是却是线程不安全的,应该避免这样写。
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);
}
建造者模式的最后一个优势在于,建造者可以传给一个方法,使该方法可以创建一个或多个对象,而且该
方法并不需要知道任何关于对象创建的细节。为了实现这个目标,我们通常使用一个简单的接口,如下:
public interface Builder {
T build();
}
在上面的例子中,我们可以让UserBuilder
实现Builder
,然后可以这样使用:
UserCollection buildUserCollection(Builder<? extends User> userBuilder){...}
在buildUserCollection
方法中,我们直接调用userBuilder
的build
方法来获取User
,而不需要知道任何关于User
对象创建的细节。
总结一下,建造者模式是一个非常棒的选择,特别是当一个类有多个参数(大于4个参数可能就是一个明显的特征),而且大多数的
参数是可选的。通过构造者模式,可以使客户端代码更容易阅读,书写和维护。而且,可以通过不可变性来使代码更加安全。
网友评论