原文链接:http://www.javacodegeeks.com/2013/01/the-builder-pattern-in-practice.html
我不会详细介绍这个模式,因为已经有大量的文章或者书籍对该模式进行过详细的解析。我将告诉你的是为什么以及什么时候你应该考虑使用它。值得一提的是,我所介绍的这个模式和设计模式四人帮的书(《设计模式:可复用面向对象软件的基础》)里面的有些许区别。四人帮书里面介绍的生成器模式重点在抽象出对象创建的步骤,并通过调用不同的具体实现从而得到不同的结果,本文介绍的生成器模式目的在于移除因多个重载构造函数,可选的参数和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 ... }
现在假设User类的某些属性是必须的,某些属性是可选的,那么你会如何构建这样一个类的实例呢?由于所有属性都声明为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; }
好消息是这样创建类实例的方式是可行的。然而这种方法存在的问题是相当明显的。当类只有少量几个属性时,这种方式也没什么大碍,但随着类属性个数的增加,代码将变得越来越难以阅读和维护。更严重的是,对调用者而言,代码也变得更加难以使用。作为调用者,我应该使用哪个构造函数呢?是两个参数的构造函数还是三个参数的呢?如果我不显示指定可选参数的值,那它的默认值是多少呢?如果我只要设置地址而不想设置年龄和手机号码呢?在这种情况下,我需要调用具有指定参数的构造函数,并传递一个默认值给那些我不感兴趣的参数。另外,当几个参数具有相同类型时,也很容易混淆调用者的使用,第一个String类型的参数是指的手机号码还是地址呢?
那么对以上这些情况我们有其他的选择吗?我们可以遵循JavaBeans规范,定义一个默认无参构造函数,并对每个属性提供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; } }
上面这种方法貌似易于阅读和维护。作为使用者,我可以创建一个空实例,并只设置我感兴趣的属性值。哪里出错了吗?这种解决方案有两个主要的问题。第一个问题是使得该类的实例具有不连续的状态。如果你想创建一个同时具有五个属性值的类实例,那么直到所有属性值的设置函数setX被调用了,该实例才具有完整的状态。这意味着调用者应用程序的某些模块可能会看到这个实例的不完整的状态。第二个缺点是这种方案使得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 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); } } }
有几个关键点需要注意一下:
1)User类的构造函数是私有的,这意味着调用者不能直接实例化这个类;
2)这个类再次成为不可变的,所有必选的属性值都是final的并且在构造函数中设置。另外,对属性值我们只提供getters函数;
3)该模式使用Fluent Interface惯用法(参见http://martinfowler.com/bliki/FluentInterface.html)使得调用者的代码更加可读(一会儿我们会看到这样的例子);
4)该模式的构造函数只接收必选的属性值作为参数,也只有这些必选的属性值被设置为final,以此保证它们在构造函数中设置;
生成器模式具有上面介绍过的其他两个方案的优点,同时又没有它们的缺点。调用者的代码更容易编写,更重要的是,更容易阅读。我听过的对该模式唯一的批评是你必须在builder内部类中重复外部类的属性定义。然而,鉴于builder类通常是它所构建的类的静态内部类,它们可以很容易同步的修改。
现在,试图创建User实例的调用者代码看起来是怎样的呢?如下所示:
public User getUser() { return new User.UserBuilder('Jhon', 'Doe') .age(30) .phone('1234567') .address('Fake address 1234') .build(); }
相当简洁,不是吗?你可以使用一行代码就创建User实例,更重要的是,代码易于阅读。而且你可以保证任何时候获取的User实例都处于完整的状态。这个模式相当灵活,一个builder可以在调用build函数之前通过设置不同的属性值来创建不同的类实例。builder甚至可以在每次调用之间自动补全生成的字段,例如id值或者序列号等。重要的一点是,类似构造函数,builder能够使得其参数为不可变的。build函数能够检查这些不可变参数并在参数无效时抛出IllegalStateException异常。
参数是从builder类拷贝到外部类,并在外部类而不是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的实例,然后才对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 extends User> { T build(); }
在前面的User例子中,UserBuilder类需要改为实现Builder<User>接口,这样一来,build函数类似如下所示:
UserCollection buildUserCollection(Builder<? extends User> userBuilder){...}
好吧,这是我写过的第一篇长博客,总结一下,生成器模式对于具有多于几个参数(并不精准,通常对于具有4个或者以上属性的类我会使用这个模式)的类的构造是个很好的选择,特别是当这些属性多数是可选的时候。应用这个模式可以使得调用者代码易于阅读,编写和维护。此外,由于你的类是不可变的你的代码将更加安全。
更新:如果你使用Eclipse作为你的IDE,那么你可以使用不少插件来简化该模式引入的样板代码的编写。我知道的三个插件如下:
1)http://code.google.com/p/bpep/
2)http://code.google.com/a/eclipselabs.org/p/bob-the-builder/
3)http://code.google.com/p/fluent-builders-generator-eclipse-plugin/
我自己没有试用过这些插件,所以不能下结论说哪一个更好用。我想其他IDE也存在类似的插件。