关于Builder模式,我不打算深入的探讨太多的细节。因为有太多关于Builder模式讲解的很好的文章和书。相反,我要告诉你为什么和什么时候你应该考虑使用它。但是,这里有必要提及的是这里描述的模式和Gang of Four book中描述有点不一样。原版的Builder模式聚焦于抽象构建步骤,所以可以通过变换不同的Builder实现的方式,我们可以获得不同的结果,而这篇文章是讲解如何通过Builder模式移除因多个构造函数,多个可选参数和过度使用的setters造成的不必要的复杂度。
设想你有如下一个有许多属性的User类。我们假设你想要使这个类不可变(顺便说下,除非有一个好的理由驱使你不用这样做。但我们将在另外一篇文章说明它)
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,你不得不在构造函数内初始化它们,但是你又想要给使用这个类的客户忽略可选属性初始化的机会。
第一个且有效的选择是写一个构造器带有所有必要属性作为参数,下一个构造器在带有所有必要属性在加一个可选的属性,另外一个带有两个可选属性,以此类推。这样写看起来是什么样子的呢?如下所示:
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 是电话号码还是地址?
那么针对这些情况我们还有其他选择吗?我们可以总是遵守javabean规范,我们设置一个默认无参构造器和为每一个属性设置setters和getters,如下所示:
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;
}
}
这个方法好像很容易阅读和维护。作为使用者我可以仅仅创建一个空的对象,然后仅仅为我感兴趣的属性设置值。那么这种方式有什么问题呢?这个方式主要有两个问题。第一个问题是实例化这个类的时候是处在一种不一致的状态。如果你想要创建一个User对象并为所有5个属性赋值,然而直到所有的setX方法被调用之前,对象没有一个完整的状态。这意味着客户端程序可能使用这个假定已经构造好了的对象,实际上是还没有构造好的对象。第二个问题是这种方法的缺点是User这个类现在是可变的。你失去了不可变对象的所有好处。
幸运地是针对这些情况,这里有第三种选择,Builder模式。这种方法看起来如下所示:
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,意味这个类无法直接被使用者实例化。
- 这个类又变成不可变的。所有的属性是final且它们都是在构造器中完成初始化。还有就是我们只为属性提供getters方法。
- Builder使用了菊花链使客户端代码更易读(稍后我们看一个例子)。
- Builder构造器只接收必要的属性且在Builder类中只有这些属性被定义为final,确保这些属性值在构造器完成初始化。
Builder模式的使用拥有我在开头提到两个方式的所有优点,且没有它们的缺点。客户端程序员容易写且更重要的是容易阅读了。我听到的唯一的批评是关于该模式下,在Builder类中你不得不复制类属性。但是,在给定的Builder类通常是创建类中一个静态成员类,它们可以很好的一起进化。
现在,客户端代码尝试创建一个新的User对象看起来是什么样?如下所示:
public User getUser() {
return new User.UserBuilder('Jhon', 'Doe')
.age(30)
.phone('1234567')
.address('Fake address 1234')
.build();
}
十分整洁,难道不是吗?你可以在一行代码中构建一个User对象,更重要的是,非常容易阅读。而且,你可以确保无论什么时候获取这个类的对象,都不会是处在一个不完整状态了。
这个模式是很灵活的。在调用build方法之间通过改变builder的属性,单个builder可以被用于创建多个对象。builder甚至可以在每次调用自动完成一些自动生成的字段,例如id或者序列号。
例如构造器,一个重要的点是builder会给参数施加不变性。build方法可以检查这些不变性的变量且如果它们是无效的可以抛出一个IllegalStateException。关键点是它们是在builder对象赋值到构建对象完成以后才被检查,因为builder是一个非线程安全的,如果我们检查参数是在正在创建对象之前,刚好在参数检查和赋值操作之间,他们的值可能被另外一个线程改变。这段时间被称为window of vulnerability。这种情况可以看下面给出的User例子:
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);
}
这个模式的最后一个优势是一个builder对象可以被传进去一个方法内,使一个方法可以为使用者创建一个或更多的对象,不需要方法知道任何关于如何创建对象的细节。如果要这样做,你通常需要一个简单的接口:
public interface Builder <T> {
T build();
}
拿前面User例子,UserBuilder实现Builder,然后看如下所示:
UserCollection buildUserCollection(Builder<? extends User> userBuilder){...}
好了,这个一篇相当长的文章。总结下,在超过一定的参数的类的时候,Builder模式是一个非常好的选择(我通常是在带有4个属性的时候使用这模式,这并一定是准确的科学指标),特别是如果这些参数大多数是可选的。你使客户端代码更容易阅读、书写和维护。此外,你的类可以保持不变性,这个使你的代码更安全。
更新:如果你使用Eclipse作为你的IDE,事实证明你可以有几款插件可以避免无聊的代码并使用Builder模式。我找到如下三款插件:
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应该也有。
Reference: The builder pattern in practice from our JCG partner Jose Luis at the Development the way it should be blog.