当前位置: 首页 > 工具软件 > Builder > 使用案例 >

优雅地创建复杂对象 —— Builder 模式

寇升
2023-12-01

摘要:
  
  当我们需要创建一个复杂的对象时,使用静态工厂或者构造器的方式就显得特别笨拙和丑陋,因为它们有个共同的局限性:它们都不能很好地扩展到大量的可选参数,也就是说,灵活性很差。那么,对于这样的类,我们应该如何创建对象呢?本文列举了三种解决办法:重叠构造器模式、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模式

 类似资料: