建造者模式实践(二)

2014-11-23 19:07:44 · 作者: · 浏览: 36
er.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方法。
  • builder类使用流式接口风格,让客户端代码阅读起来更容易(我们马上就会看到一个它的例子)。
  • builder类构造方法只接收必须属性,为了确保这些属性在构造方法里赋值,只有这些属性被定义成final类型。

    使用建造者模式有在本文开始时提到的两种方法的所有优点,并且没有它们的缺点。客户端代码写起来更简单,更重要的是,更易读。我听过的关于该模式的唯一批判是你必须在builder类里面复制类的属性。然而,考虑到这个事实,builder类通常是需要建造的类的一个静态类成员,它们一起扩展起来相当容易。

    现在,试图创建一个新的User对象的客户端代码看起来如何那?让我们来看一下:

    public User getUser() {
        return new
                User.UserBuilder('Jhon', 'Doe')
                .age(30)
                .phone('1234567')
                .address('Fake address 1234')
                .build();
    }
    

    非常整洁,是不是?你可以只用一行代码就创建一个User对象,更重要的是,代码很易读。此外,我们也确保无论何时你获得一个该类的对象,该对象都不会是一个不完整的状态。

    这个模式是非常灵活的。一个单独的builder类可以通过在调用build方法之前改变builder的属性来创建多个对象。builder类甚至可以在每次调用之间自动补全一些生成的字段,例如一个id或者序列号。

    很重要的一点是,例如构造方法,builder类可以在构造方法参数上增加约束。build方法可以检查这些约束,如果不满足就抛出一个IllegalStateException异常。

    至关重要的是要在builder的参数拷贝到建造对象之后在验证参数,这样验证的就是建造对象的字段,而不是builder的字段。这么做的原因是builder类不是线程安全的,如果我们在创建真正的对象之前验证参数,参数值可能被另一个线程在参数验证完和参数被拷贝完成之间的时间修改。这段时间周期被称作“脆弱之窗”。
    在我们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 build();
    }
    

    借用之前的User例子,UserBuilder类可以实现Builder。如此,我们可以有如下的代码:

    UserCollection buildUserCollection(Builder
          userBuilder){...}
    

    好吧,这确实是一篇很长的文章,虽然是第一次发。总而言之,建造者模式在多于几个参数(虽然不是很科学准确,但是我通常把四个参数作为使用建造者模式的一个很好的指示器),特别是当大部分参数都是可选的时候。你可以让客户端代码在阅读,写和维护方面更容易。另外,你的类可以保持不可变特性,让你的代码更安全。

    UPDATE:如果你使用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/

      这几个插件我都没有使用过,所以关于哪个更好,我无法给出一个有经验的决定。我估计其他IDEs也会存在类型的插件。