连不及格的小明都学会了建造者模式

建造者模式

使用Java的我们,每天都是在写出一个类,然后去new出一个对象。我们使用最多的便是通过类的构造器来创建出一个新的对象,但是在某些情境下,可能单纯的使用构造器并非最好的选择。

情景

我们有一个学生类,有必填属性:学号,姓名。选填属性:性别,年龄,语文成绩,数学成绩,英语成绩。要求我们构造出这个学生类。

方式一:单纯使用构造器

最简单的方式是我们不去管所谓的必填和可选字段,直接无脑一个构造器,代码如下:

public class Student{

    //必填字段
    private final long id;
    private final String name;

    //选填字段
    private final boolean sex;
    private final int age;
    private final double chinese;
    private final double math;
    private final double english;

    public Student(long id,String name,boolean sex,int age,double chinese,double math,double english){
        this.id = id;
        this.name = name;
        this.sex = sex;
        this.age = age;
        this.chinese = chinese;
        this.math = math;
        this.english = english;
    }

}

此时我们假设我们需要创建一个对象,他有id,姓名,那么我们的调用将会是这样的

Student stu = new Student(1, "小明", true, 0, 0, 0, 0);

我们仅仅想传入必填字段(id,姓名),却需要填入5个不相干的字段,这还是属性比较少情况,请想象如果是30个字段呢,在使用这种方式创建对象的时候是非常容易出错的。

方式二:重叠构造器

首先我们分析出学生类构造器最少需要2个参数(学号,姓名),最多7个参数,那么我们可以使用重叠构造器的方式,Student类实现如下:

public class Student{

    //必填字段
    private final long id;
    private final String name;

    //选填字段
    private final boolean sex;
    private final int age;
    private final double chinese;
    private final double math;
    private final double english;

    //必须字段构造器
    public Student(long id,String name){
        this(id,name,true);
    }
    //1个可选字段构造器
    public Student(long id,String name,boolean sex){
        this(id,name,sex,0);
    }
    //2个可选字段构造器
    public Student(long id,String name,boolean sex,int age){
        this(id,name,sex,age,0.0d)
    }
    //3个可选字段构造器
    public Student(long id,String name,boolean sex,int age,double chinese){
        this(id,name,sex,age,chinese,0.0d);
    }
    //4个可选字段构造器
    public Student(long id,String name,boolean sex,int age,double chinese,double math){
        this(id,name,sex,age,chinese,math,0.0d);
    }
    //完整字段构造器
    public Student(long id,String name,boolean sex,int age,double chinese,double math,double english){
        this.id = id;
        this.name = name;
        this.sex = sex;
        this.age = age;
        this.chinese = chinese;
        this.math = math;
        this.english = english;
    }

}

我们使用重叠构造器的目的是为了尽量减少像方式一中传入部分字段时其他不相干字段的数量,我们从必填字段开始创建一个仅包含必填字段的构造器,然后依次创建包含1个可选字段的构造器、2个可选字段的构造器、···、全部字段的构造器。这时当我们创建方式一种的对象时,代码是这样的:

Student stu = new Student(1, "小明");

好像是简单了不少,那么要是小明一不小心英语考了59分呢Ծ‸Ծ

Student stu = new Student(1, "小明", true, 0, 0, 0, 59.0d);

(⊙o⊙)…好像又变得老长了···

不着急,我们不是还有setter方法嘛!

方式三:setter方法

这是我们平常开发中用到最多的方式,直接看代码吧

public class Student{

    //必填字段
    private final long id;
    private final String name;

    //选填字段
    private boolean sex =true;
    private int age = 0;
    private double chinese = 0.0d;
    private double math = 0.0d;
    private double english = 0.0d;

    //仅包含必填字段的构造器
    public Student(long id,String name){
        this.id = id;
        this.name = name;
    }

    public void setId(long id){
        this.id = id;
    }

    public void setName(String name){
        this.name = name;
    }

    //··· 还怪长咯 我不写了 脑补一哈啊

    
}

我们要为我们的选填字段设定默认值,然后写一个仅包含必填字段的构造器,其他的选填字段都使用setter方法设置,这时候我们再次把那个不及格的小明创建出来。

Student stu = new Student(1, "小明");
stu.setEnglish(59.0d);

构造器传值不用传一串了,而且也挺容易看懂的。

但是,如果我们增加一个出生年呢,可以想象,出生年和年龄是有关系,我们不能瞎填,而我们使用setter方法,将Student这个对象的构造过程分到了几个调用过程中,我们想去判断年龄和出生年就比较麻烦了。你可能会说,可以在setter方法中判断啊,对,是可以,但是我们至少需要在出生年和年龄上面都进行判断,因为我们不知道两个setter方法究竟谁会被先调用,而且这还是比较简单的,如果涉及大量字段的逻辑判断,那么我们将所有的字段的setter方法都去实现这个判断,还去考虑调用顺序问题,显然这回让人崩溃的。这种方式使得我们将Student类变为不可变模式变得不可能。

而建造者(Builder)模式恰好就解决了我们面临的问题:既简化了构造对象时的方法调用,又保证了构造安全。

方式四:建造者(Builder)模式

我们使用建造者模式,重写了我们Student类

public class Student {

    // 必填字段
    private final long id;
    private final String name;

    // 选填字段
    private final boolean sex;
    private final int age;
    private final double chinese;
    private final double math;
    private final double english;

    //建造者内部类
    public static class Builder {
        // 必填字段
        private final long id;
        private final String name;

        // 选填字段 指定默认值
        private boolean sex = true;
        private int age = 0;
        private double chinese = 0.0d;
        private double math = 0.0d;
        private double english = 0.0d;

        //构建者构造器 使用必填字段
        public Builder(long id,String name){
            this.id = id;
            this.name = name;
        }

        //设置其他可选字段
        public Builder isGirl(){
            this.sex = false;
            return this;
        }
        public Builder age(int age){
            this.age = age;
            return this;
        }
        public Builder chinese(double chinese){
            this.chinese = chinese;
            return this;
        }
        public Builder math(double math){
            this.math = math;
            return this;
        }
        public Builder english(double english){
            this.english = english;
            return this;
        }

        //调用Student构造器 使用建造者的属性完成Student对象的创建
        public Student build(){
            return new Student(this);
        }


    }

    //私有化构造方法
    private Student(Builder builder){
        this.id = builder.id;
        this.name = builder.name;
        this.sex = builder.sex;
        this.age = builder.age;
        this.chinese = builder.chinese;
        this.math = builder.math;
        this.english = builder.english;
    }

}

首先Student类的所有字段都被final修饰,这在setter方法中显然不可能实现,因为我们不能使用setter方法去改变一个不可变的字段。而建造者模式恰恰避开了使用setter方法带来的设置可选字段的弊端(构造不安全)同时还享受了使用setter方法带来的构造便捷。Student类的所有字段都是不可变的,而我们的Builder内部类的可选字段时可变的,这样我们就可以在Builder对象构建的时候去灵活的设定可变字段的值,而我们的改变字段的值仅仅放生在Builder对象构建过程中中,改变的也仅仅是Builder对象的字段,只要我们不调用build()方法,那么这一切改变都与Student对象无关。讲白了就是进行延迟构造,这种方式是不是特别的熟悉(想想Java8中的Stream有关的操作是不是有延迟调用的特点)。

再次邀请我们不及格的小明登场

Student stu = new Student.Builder(1, "小明").english(59.0d).build();

代码简洁极了!!!方式三中说的年龄和出生年类似的问题其实已经解决了。想一想,这种问题的产生的原因是什么,对,就是setter带来的构造多次调用,我们转过来看使用建造者模式的Student类,他的构造方法/build()可是将所有字段都包含了哦,也就是Student对象将会是一次性构造出来,我们的逻辑判断不用再考虑调用顺序问题了(p≧w≦q),直接在构造方法/build()中完成就OK了!!!

有错误的地方希望大家指正Thanks♪(・ω・)ノ

写于2020年4月30日12:38:11

原文地址:https://www.cnblogs.com/erkye/p/12808169.html