建造者模式实践

我不打算跳入设计模式的过多细节中,因为已经有一大堆的文章和书籍很详细的解释过了。所以我打算告诉你为什么和什么时候你应该考虑使用设计模式。然而,值得一提的是本文中的模式和四人帮的《Design Patterns: Elements of Reusable Object-Oriented Software》一书中的提出的有点不一样。因为原生模式专注于抽象构建的步骤,所以通过使用不同的建造者的实现我们能得到不同的结果,然而在本文中解释的设计模式是讨论如何移除源自于多构造函数,多可选参数和滥用的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类型参数对应的是phone还是address?

因此我们有什么其他选择来应对这种场景?我们可以总是遵循JavaBeans惯例,有一个默认的无参构造方法并且每个属性都有getter和setter方法。就象这样:

    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个属性都要赋值,那么直到所有的setXX方法都被调用之前,该对象都没有一个完整的状态。这意味着在该对象状态还不完整的时候,一部分客户端程序可能看见这个对象并且以为该对象已经构造完成。这种方法的第二个不足是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);
        }
        
    }
}

一些值得注意的关键点:

  • 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<? extends User> 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也会存在类型的插件。

原文链接: javacodegeeks 翻译: ImportNew.com - kingviker
译文链接: http://www.importnew.com/11506.html
[ 转载请保留原文出处、译者和译文链接。]



相关文章

发表评论

Comment form

(*) 表示必填项

4 条评论

  1. 不是柯西 说道:

    感谢

    Thumb up 0 Thumb down 0

  2. Jcdroid 说道:

    很不错

    Thumb up 0 Thumb down 0

  3. test 说道:

    不错,google protobuf中见到过这种用法

    Thumb up 0 Thumb down 0

  4. abcedf 说道:

    写的非常的好,谢谢

    Thumb up 0 Thumb down 0

跳到底部
返回顶部