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

转载自:https://blog.csdn.net/justloveyou_/article/details/78298420

摘要:
  
  当我们需要创建一个复杂的对象时,使用静态工厂或者构造器的方式就显得特别笨拙和丑陋,因为它们有个共同的局限性:它们都不能很好地扩展到大量的可选参数,也就是说,灵活性很差。那么,对于这样的类,我们应该如何创建对象呢?本文列举了三种解决办法:重叠构造器模式、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 + '\'' +
                '}';
    }
}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61

  使用这种模式创建对象时,存在一下几点不足:

  • 灵活性很差:如果客户端只想创建一个给定姓名,性别,生日,邮箱和体重的人,那么他将调用如下构造函数,这样无意中就“被迫”设置了他本不想设置的一些参数。
    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);
    }
   
   
  • 1
  • 2
  • 3
  • 4
  • 代码难以编写与阅读:当属性有很多的时候,代码不但看起来很丑陋,而且极易出错。试想,若客户端不小心颠倒了参数列表中两个参数的顺序 (例如,颠倒了参数“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 + '\'' +
                '}';
    }
}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63

  这种方式虽然保证了灵活性,也不易出错,例如:

        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);
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

  但是其本身也存在这一些固有的缺点,比如:

  • 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 + '\'' +
                '}';
    }
}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95

  我们可以通过下面的方式来创建一个Person对象:

        Person.Builder builder = new Person.Builder("rico", "boy", new Date(), "rico@tju.edu.cn");
        Person p1 = builder.height(173).addr("天津市").nickName("书呆子").build();
   
   
  • 1
  • 2

  显而易见,使用这种方式创建对象不但灵活而且易于阅读,且不易出错。总的来说,这种模式具有以下特点:

  • 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;
        }
    }
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

  需要特别注意的是,我们是对Person对象进行参数检查,而不是对Builder对象进行参数检查,因为Builder对象不是线程安全的,即下面的代码存在线程安全问题:

    public Person build(){
        if (!"boy".equals(this.sex)){
            throw new IllegalArgumentException("所注册用户必须为男性!");
        }else{
            return new Person(this);
        }
    }
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

六. 总结

(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模式

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值