这是我第一篇文章(也是我关于这个主题的第一篇博客)。我记不清在哪读过这项内容(尽管我基本上确认是在Practices of an Agile Developer上看到的),但是写博客应该能帮助你全神贯注。具体点来说,通过花些时间来解释你所知道的东西,你能更好的理解它。


这也正是我想要努力去做的,通过解释一件事,继而进一步理解这件事。并且还有个额外的好处,当我回忆曾经做过的事情时,它是一个很好的集中地。希望在这过程中也能帮助到你们。


废话不多,让我们直奔主题——构造模式。我不打算分割成许多细节来讲,因为已经有非常多的稿件,书籍详细的说明过这个模式。 反而,我会告诉你为什么,以及什么时候应该考虑使用它。然而,值得一提的是,这里所说的模式和四人帮书中的模式有些不同。尽管原始的模式聚焦于抽象出构建的步骤,这样通过改变建造者的实现就可以得到不同的结果,本篇所说的模式着眼于从多构造器,多可选参数以及过度使用的setter中移除不必要的复杂性。


想象下你有一个类,像下图所示有许多属性。假设你想让你的类不可变(顺便说一下,除非有一个好的理由不这样做,否则你应该坚持。但是我们会以另一种方式来达到要求。)


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;

}


这种方式来构建类的实例的好处是它能很好的工作。然而,这种方式的问题也很明显。当你只有几个属性时还好,但是当这个数字扩大时,代码就变的难以理解和维护了。


更重要的是,代码对客户端来说变的很难。客户端应该调用哪个构造器?有两个参数的?有三个参数的?那些不用传确切值的参数的默认值是多少?如果我想给地址赋值,但是不给age和phone赋值要怎么办?那种情况下,我就不得不调用接受所有参数的构造器,并且给那些不需要的传入不在乎的默认值。此外,几个类型相同的参数是很令人费解的。第一个String是电话还是地址? 那么在这些情况下,我们还有其他选择吗?我们可以依照JavaBeans的约定,一个无参构造并且每个参数提供一个get和set。类似下面这个:


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;

  }

}


这种方式看起来容易理解和维护。作为客户端,我只需要创建一个空对象并且set我感兴趣的属性即可。那么这种方式有什么弊端呢?有两个主要弊端。第一个是类 实例的不一致状态。如果你要用User的五个属性来创建一个User对象,那么在所有的setX方法调用前,对象处于不完全状态。这就意味着客户端的其他 部分可能看到对象,并且假设它已经完成构造了,实际它并没有。方法的第二个缺点是对象可变。你丧失了不可变对象的所有好处。


幸运的是还有第三个选择,建造者模式,方案看起来是下面这样的。


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操作。

  • 建造者使用流式接口习语来让客户端代码更易读(下面会有示例)。

  • 建造者的构造器只接受两个必须的参数,并且这两个属性是仅有的被设置为final类型的,这样就能保证这些属性在构造器中是被赋值的。


建造者模式的使用拥有开始所提两种方案的所有优点,并且没有它们的缺点。客户代码更容易写,最重要的是更易读。关于这个模式,我听到的唯一缺点是必须要复制类的属性到建造者中。既然建造者类通常是它所建造类的一个静态成员类,它们能相当容易的一起演进。


那么,客户代码尝试创建一个新的User对象会是什么样的?让我们来看看:


public User getUser() {

  return new

    User.UserBuilder("Jhon", "Doe")

    .age(30)

    .phone("1234567")

    .address("Fake address 1234")

    .build();

}


很工整,不是吗?你能在一行内创建一个User对象,最重要的是它很容易理解。而且,你能确保,无论什么时候你拿到这个类的一个对象,它的状态都是完整的。


这个模式非常灵活。一个建造者可以通过在多次调用“build”之间改变属性用来创建多个对象。构造者甚至可以在两次调用之间自动补全一些生成的字段。例如id或其他序列号。


重点是,类似于构造器,建造者可以强制其参数的不变性。建造方法可以检查这些不变性, 如果它们无效就抛出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){...}


这真是个很长的首发文章。总结一下,建造者模式是处理超过一个参数的类的绝佳选择(这不是严格意义上的说法,但是我通常将接受四个属性的类当成使用这种模式的暗示),特别是如果大部分的参数是可选的。你的客户端代码会更易读,易写,易维护。此外,你的类可以保持不变,这点可以让你的代码更安全。


更新:如果你使用Eclipse作为你的IDE,有一些插件可以让你避免建造者中大部分的官样文章代码。下面这三个是比较推荐的:


  • 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应该也有类似的插件。


更多相关文章

  1. 一个GCRoot不可达的对象,会立刻被垃圾回收吗?
  2. Object对象你真理解了吗?
  3. HotSpot VM 中对象的内存分析
  4. 从对象生命周期的经验统计到垃圾回收算法
  5. 什么样的 Java 对象会被当垃圾回收?
  6. Spring Ioc 实例化 Bean 对象有几种方式?
  7. Javascript面向对象入门
  8. 如何在 Java 中构造对象(学习 Java 编程语言 034)

随机推荐

  1. 【android基础】之Android返回键处理(事
  2. Android之XUtils的框架总结
  3. eclipse下android的sdk配置问题
  4. 我是如何自学Android,资料分享(2015 版)
  5. Android中的Shape美化
  6. Android(安卓)TextView文字横向自动滚动(
  7. Android 技术专题系列之三 -- 编译(build)
  8. Android 相对布局:RelativeLayout
  9. Android分区解释:boot, system, recovery,
  10. Android(安卓)init 启动过程分析1