天道不一定酬所有勤
但是,天道只酬勤
Hollis出品的全套Java面试宝典不来了解一下吗?

设计模式(十)——建造者模式的实践

Hollis出品的全套Java面试宝典不来了解一下吗?

新专题:设计模式,我会在博客(http://www.hollischuang.com)及微信公众号(hollischuang)同步更新,欢迎共同学习。

本文由HollisChuang 翻译自 The builder pattern in practice . 原作者:Jose Luis

我不打算深入介绍设计模式的细节内容,因为有很多这方面的文章和书籍可供参考。本文主要关注于告诉你为什么以及在什么情况下你应该考虑使用建造者模式。然而,值得一提的是本文中的模式和GOF中的提出的有点不一样。那种原生的模式主要侧重于抽象构造的过程以达到通过修改builder的实现来得到不同的结果的目的。本文中主要介绍的这种模式并没有那么复杂,因为我删除了不必要的多个构造函数、多个可选参数以及大量的setter/getter方法。

假设你有一个类,其中包含大量属性。就像下面的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
    ...
}

在这样的类中,有一些属性是必须的(required)而另外一些是可选的(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赋值而不对agephone赋值怎么办?遇到这种情况可能我只能调用那个参数最全的构造函数,然后对于我不想要的参数值传递一个默认值。此外,如果多个参数的类型都相同那就很容易让人困惑,第一个String类型的参数到底是number还是address呢?

还有没有其他方案可选择呢?我们可以遵循JaveBean规范,定义一个只包含无参数的构造方法和gettersetter方法的JavaBean

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;
    }
}

这种方式看上去很容易阅读和维护。对于调用者来说,我只需要创建一个空的对象,然后对于我想设置的参数调用setter方法设置就好了。这难道还有什么问题吗?其实存在两个问题。第一个问题是该类的实例状态不固定。如果你想创建一个User对象,该对象的5个属性都要赋值,那么直到所有的setXX方法都被调用之前,该对象都没有一个完整的状态。这意味着在该对象状态还不完整的时候,一部分客户端程序可能看见这个对象并且以为该对象已经构造完成。这种方法的第二个不足是User类是易变的(因为没有属性是final的)。你将会失去不可变对象带来的所有优点。

幸运的是应对这种场景我们有第三种选择,建造者模式。解决方案类似如下所示:

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类通常是需要建造的类的一个静态类成员,它们一起扩展起来相当容易。(译者表示没明白为设定为静态成员扩展起来就容易了。设为静态成员我认为有一个好处就是可以避免出现is not an enclosing class的编译问题,创建对象时候更加方便

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

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

译者注:如果UserBuilder没有设置为static的,以上代码会有编译错误。错误提示:User is not an enclosing class

以上代码看上去相当整洁。我们可以只通过一行代码就可以创建一个User对象,并且这行代码也很容易读懂。除此之外,这样还能确保无论何时你想获取该类的对象都不会是不完整的(译者注:因为创建对象的过程是一气呵成的,一旦对象创建完成之后就不可修改了)。

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

值得注意的是,像构造函数一样,builder可以对参数的合法性进行检查,一旦发现参数不合法可以抛出IllegalStateException异常。

但是,很重要的一点是,如果要检查参数的合法性,一定要先把参数传递给对象,然后在检查对象中的参数是否合法。其原因是因为builder并不是线程安全的。如果我们在创建真正的对象之前验证参数,参数值可能被另一个线程在参数验证完和参数被拷贝完成之间的时间修改。这段时间周期被称作“脆弱之窗”。我们的例子中情况如下:

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开发,你有很多插件来避免编写建造者模式大部分的重复代码。已知的有下面三个:

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

赞(5)
如未加特殊说明,此网站文章均为原创,转载必须注明出处。HollisChuang's Blog » 设计模式(十)——建造者模式的实践
Hollis出品的全套Java面试宝典不来了解一下吗?

评论 1

  1. #1

    最后一个点,是不是有点类似于Command模式,将任务执行的出发,与任务逻辑的实现解开。

    另外,其实lombok的@Builder注解也很好用,在IntelliJ中还能用delombok的功能把这个注解转换为代码满足各种需求。

    Zzzzzzac8年前 (2016-06-11)回复

HollisChuang's Blog

联系我关于我