摘要:
当我们需要创建一个复杂的对象时,使用静态工厂或者构造器的方式就显得特别笨拙和丑陋,因为它们有个共同的局限性:它们都不能很好地扩展到大量的可选参数,也就是说,灵活性很差。那么,对于这样的类,我们应该如何创建对象呢?本文列举了三种解决办法:重叠构造器模式、JavaBeans模式和Builder模式,并通过具体实例对上述三种方法进行铺垫和对比,从而真正帮助读者理解Builder模式。
本文原创作者:书呆子Rico
作者博客地址:http://blog.csdn.net/justloveyou_/
一. 动机
当我们需要创建一个复杂的对象时,使用静态工厂或者构造器的方式就显得特别笨拙和丑陋,因为它们有个共同的局限性:它们都不能很好地扩展到大量的可选参数。考虑用一个Person类来描述一个人,除了姓名,性别,生日,邮箱等必要的属性外,还有很多可选的属性,比如:身高,学历,绰号,体重,通讯地址等等。对于这样的类,我们应该如何创建对象呢?无论是常见的重叠构造器模式还是JavaBeans模式,它们都不能很好地解决这类问题,而我们本文所着重阐述的Builder模式则正好是解决此类问题的利剑。为了更深入的了解Builder模式所带来的好处,我们先分别采用重叠构造器模式和JavaBeans模式来解决上述问题。
二. 使用重叠构造器模式创建复杂对象
在这种模式下,我们提供的第一个构造器只有必要的参数,第二个构造器有一个可选参数,第三个构造器有两个可选参数,以此类推,最后一个构造器含有所有参数,如下所示:
- public class Person {
- private String name; // required
- private String sex; // required
- private Date date; // required
- private String email; // required
- private int height; // optional
- private String edu; // optional
- private String nickName; // optional
- private int weight; // optional
- private String addr; // optional
- public Person(String name, String sex, Date date, String email) {
- this(name, sex, date, email, 0);
- }
- public Person(String name, String sex, Date date, String email, int height) {
- this(name, sex, date, email, height, null);
- }
- public Person(String name, String sex, Date date, String email, int height, String edu) {
- this(name, sex, date, email, height, edu, null);
- }
- public Person(String name, String sex, Date date, String email, int height, String edu, String nickName) {
- this(name, sex, date, email, height, edu, nickName, 0);
- }
- public Person(String name, String sex, Date date, String email, int height, String edu, String nickName, int
- weight) {
- this(name, sex, date, email, height, edu, nickName, weight, null);
- }
- public Person(String name, String sex, Date date, String email, int height, String edu, String nickName, int
- weight, String addr) {
- this.name = name;
- this.sex = sex;
- this.date = date;
- this.email = email;
- this.height = height;
- this.edu = edu;
- this.nickName = nickName;
- this.weight = weight;
- this.addr = addr;
- }
- @Override
- public String toString() {
- return "Person{" +
- "name='" + name + '\'' +
- ", sex='" + sex + '\'' +
- ", date=" + date +
- ", email='" + email + '\'' +
- ", height=" + height +
- ", edu='" + edu + '\'' +
- ", nickName='" + nickName + '\'' +
- ", weight=" + weight +
- ", addr='" + addr + '\'' +
- '}';
- }
- }
使用这种模式创建对象时,存在一下几点不足:
- 灵活性很差:如果客户端只想创建一个给定姓名,性别,生日,邮箱和体重的人,那么他将调用如下构造函数,这样无意中就“被迫”设置了他本不想设置的一些参数。
- public Person(String name, String sex, Date date, String email, int height, String edu, String nickName, int
- weight) {
- this(name, sex, date, email, height, edu, nickName, weight, null);
- }
- 代码难以编写与阅读:当属性有很多的时候,代码不但看起来很丑陋,而且极易出错。试想,若客户端不小心颠倒了参数列表中两个参数的顺序 (例如,颠倒了参数“email”和“edu”),编译器也不会出错,但是在运行时就会出现错误的行为,并且这种错误难以发现。
三. 使用JavaBeans模式创建复杂对象
这时,我们可能转而求助于JavaBeans模式来避免这些问题,但是同来也会带来一些新的问题。同样的例子,若我们采用JavaBeans模式,那么代码将会是如下的样子:
- public class Person {
- private String name; // required
- private String sex; // required
- private Date date; // required
- private String email; // required
- private int height; // optional
- private String edu; // optional
- private String nickName; // optional
- private int weight; // optional
- private String addr; // optional
- public void setName(String name) {
- this.name = name;
- }
- public void setSex(String sex) {
- this.sex = sex;
- }
- public void setDate(Date date) {
- this.date = date;
- }
- public void setEmail(String email) {
- this.email = email;
- }
- public void setHeight(int height) {
- this.height = height;
- }
- public void setEdu(String edu) {
- this.edu = edu;
- }
- public void setNickName(String nickName) {
- this.nickName = nickName;
- }
- public void setWeight(int weight) {
- this.weight = weight;
- }
- public void setAddr(String addr) {
- this.addr = addr;
- }
- @Override
- public String toString() {
- return "Person{" +
- "name='" + name + '\'' +
- ", sex='" + sex + '\'' +
- ", date=" + date +
- ", email='" + email + '\'' +
- ", height=" + height +
- ", edu='" + edu + '\'' +
- ", nickName='" + nickName + '\'' +
- ", weight=" + weight +
- ", addr='" + addr + '\'' +
- '}';
- }
- }
这种方式虽然保证了灵活性,也不易出错,例如:
- Person p2 = new Person();
- p2.setName("livia");
- p2.setSex("girl");
- p2.setDate(new Date());
- p2.setEmail("livia@tju.edu.cn");
- p2.setHeight(163);
- p2.setEdu("NCU");
- p2.setNickName("pig");
- p2.setWeight(100);
- p2.setAddr("北京市");
- System.out.println(p2);
但是其本身也存在这一些固有的缺点,比如:
- Setter的存在妨碍了其成为不可变类的可能:这样,在并发环境下,我们就不得不考虑其线程安全性;
- 代码丑陋且对象易处于不一致状态:上面创建对象的方式也比较丑陋,同时由于对象的构造过程分为若干个函数调用,所以容易导致对象处于不一致状态。
四. 使用Builder模式创建复杂对象
使用Builder模式创建复杂对象,不但可以避免上述两种方式的缺点,而且还能兼顾们各自的优点。该模式的内涵是:不直接生成想要的对象,而是让客户端利用所有必要的参数构造一个Builder对象,然后在此基础上,调用类似于Setter的方法来设置每个可选参数,最后通过调用无参的build()方法来生成不可变对象。一般地,所属Builder是它所构建类的静态成员类。代码如下:
- public class Person {
- private final String name; // required
- private final String sex; // required
- private final Date date; // required
- private final String email; // required
- private final int height; // optional
- private final String edu; // optional
- private final String nickName; // optional
- private final int weight; // optional
- private final String addr; // optional
- // 私有构造器,因此Person对象的创建必须依赖于Builder
- private Person(Builder builder) {
- this.name = builder.name;
- this.sex = builder.sex;
- this.date = builder.date;
- this.email = builder.email;
- this.height = builder.height;
- this.edu = builder.edu;
- this.nickName = builder.nickName;
- this.weight = builder.weight;
- this.addr = builder.addr;
- }
- public static class Builder{
- private final String name; // required,使用final修饰
- private final String sex; // required,使用final修饰
- private final Date date; // required,使用final修饰
- private final String email; // required,使用final修饰
- private int height; // optional,不使用final修饰
- private String edu; // optional,不使用final修饰
- private String nickName; // optional,不使用final修饰
- private int weight; // optional,不使用final修饰
- private String addr; // optional,不使用final修饰
- public Builder(String name, String sex, Date date, String email) {
- this.name = name;
- this.sex = sex;
- this.date = date;
- this.email = email;
- }
- // 返回Builder对象本身,链式调用
- public Builder height(int height){
- this.height = height;
- return this;
- }
- // 返回Builder对象本身,链式调用
- public Builder edu(String edu){
- this.edu = edu;
- return this;
- }
- // 返回Builder对象本身,链式调用
- public Builder nickName(String nickName){
- this.nickName = nickName;
- return this;
- }
- // 返回Builder对象本身,链式调用
- public Builder weight(int weight){
- this.weight = weight;
- return this;
- }
- // 返回Builder对象本身,链式调用
- public Builder addr(String addr){
- this.addr = addr;
- return this;
- }
- // 通过Builder构建所需Person对象,并且每次都产生新的Person对象
- public Person build(){
- return new Person(this);
- }
- }
- @Override
- public String toString() {
- return "Person{" +
- "name='" + name + '\'' +
- ", sex='" + sex + '\'' +
- ", date=" + date +
- ", email='" + email + '\'' +
- ", height=" + height +
- ", edu='" + edu + '\'' +
- ", nickName='" + nickName + '\'' +
- ", weight=" + weight +
- ", addr='" + addr + '\'' +
- '}';
- }
- }
我们可以通过下面的方式来创建一个Person对象:
- Person.Builder builder = new Person.Builder("rico", "boy", new Date(), "rico@tju.edu.cn");
- Person p1 = builder.height(173).addr("天津市").nickName("书呆子").build();
显而易见,使用这种方式创建对象不但灵活而且易于阅读,且不易出错。总的来说,这种模式具有以下特点:
- Person类的构造方法是私有的:也就是说,客户端不能直接创建User对象;
- Person类是不可变的:所有的属性都被final修饰,在构造方法中设置参数值,并且不对外提供setters方法;
- Builder模式的高可读性:Builder模式使用了链式调用,可读性更佳。
- Builder对象与目标对象的异同:Person与Builder拥有共同的属性,并且Builder内部类构造方法中只接收必传的参数,同时只有这些必传的参数使用了final修饰符。
五. Builder模式中的参数约束与线程安全性
我们知道,Person对象是不可变的,因此是线程安全的;但是,Builder对象并不具有线程安全性。因此,当我们需要对Person对象的参数强加约束条件时,我们应该可以对builder()方法中所创建出来的Person对象进行检验,即我们可以将builder()方法进行如下重写:
- public Person build() {
- Person person = new Person(this);
- if (!"boy".equals(person.sex)) {
- throw new IllegalArgumentException("所注册用户必须为男性!");
- } else {
- return person;
- }
- }
需要特别注意的是,我们是对Person对象进行参数检查,而不是对Builder对象进行参数检查,因为Builder对象不是线程安全的,即下面的代码存在线程安全问题:
- public Person build() {
- if (!"boy".equals(this.sex)) {
- throw new IllegalArgumentException("所注册用户必须为男性!");
- } else {
- return new Person(this);
- }
- }
六. 总结
(1). Builder模式的应用场景
对象属性繁多,一般都具有5个或者5个以上的属性,特别是大多数参数都是可选的时候;
(2). Builder模式与重叠构造器模式及JavaBeans模式的对比
与重叠构造器模式相比,使用Builder模式的代码更易阅读和编写,并且也比JavaBeans模式更安全;
(3). 本文所述Builder模式与GOF经典Builder模式的关系
本文所谈的Builder模式可以看作是GOF经典Builder模式的简化版,其省略掉了Director,这样结构更加简单。特别地,在很多框架源码中,涉及到Builder模式的应用大多都不是经典GOF的Builder模式而是本文中所探讨的形式,比如Hibernate中国SessionFactory的创建等等。
由于GOF经典Builder模式在实践中较少使用,故本文不再赘述。
七. 更多
更多关于 Java内部类 的介绍,请移步至笔者 《 Java 内部类综述》一文。
更多关于 并发编程与线程安全问题 的介绍,请移步笔者 《Java并发编程学习笔记》专栏。
引用:
设计模式之Builder模式