多参构造器与 Builder 模式及链式调用的运用

Posted 小羊子说

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多参构造器与 Builder 模式及链式调用的运用相关的知识,希望对你有一定的参考价值。

1. 前言:

builder 模式的运用第一反应是构造者设计模式的运用,其实也可以用于对象的构造以及优雅的链式调用。

首先,回顾标准的用法

在《Effective Java 第2版》中有提到,遇到多个构造器参数时要考虑使用构建器(Builder模式)。相比于重叠构造器(telescoping constructor)模式JavaBeans模式Builder模式实现的对象更利于使用。


/**
 * <pre>
 *     author : June Yang
 *     time   : 2021/02/06
 *     desc   : Effective Java 中builder的使用
 *     version: 1.0
 * </pre>
 */
public class NutritionFacts 
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;


    public static class Builder 
        //必须注入的参数,可不用初始化
        private final int servingSize;
        private final int servings;

        //可选择注入的参数
        private int calories = 0;
        private int fat = 0;
        private int sodium = 0;
        private int carbohydrate = 0;

        public Builder(int servingSize, int servings) 
            this.servingSize = servingSize;
            this.servings = servings;
        

        public Builder setCalories(int calories) 
            this.calories = calories;
            return this;
        

        public Builder calories(int val) 
            this.calories = val;
            return this;
        

        public Builder carbohydrate(int val) 
            this.carbohydrate = val;
            return this;
        

        public Builder sodium(int val) 
            this.sodium = val;
            return this;
        

        public NutritionFacts build() 
            return new NutritionFacts(this);
        
    

    @Override
    public String toString() 
        return "NutritionFacts" +
                "servingSize=" + servingSize +
                ", servings=" + servings +
                ", calories=" + calories +
                ", fat=" + fat +
                ", sodium=" + sodium +
                ", carbohydrate=" + carbohydrate +
                '';
    

    //私有构造方法
    private NutritionFacts(Builder builder) 
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.carbohydrate;
    
    
    public static void main(String[] args) 
        NutritionFacts bean = new NutritionFacts.
                Builder(8, 20)
                .calories(30)
                .build();
    


调用时:

   public static void main(String[] args) 
        NutritionFacts bean = new NutritionFacts.
                Builder(10, 20)
                .calories(30)
                .build();
    

2. android项目的中运用

Android开发中经常遇到这样的代码:

如网络请求

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.xxx.com/")
    .build();

或者 实例化一个对话框

new AlertDialog.Builder(this)
            .setTitle("TitleName")
            .setMessage("Message")
            .setCancelable(true)
            .setOnCancelListener(new DialogInterface.OnCancelListener() 
                @Override
                public void onCancel(DialogInterface dialog) 
                    //...
                
            )
            .show();

builder实现的链式调用看上去如此优雅。于是项目开发中我们也想实际用起来。

3. 常见的两种构建方式

在日常开发中,我们经常需要给某个对象的变量赋值,这个赋值的过程称为对象的构建。

比如现在有个 Person 类,它有几个成员变量:

//固定不变的对象,一般变量需要声明为 final
private final String mName;     //必选,final 类型需要在 构造器中初始化,不允许不初始化它的构造器存在
private String mLocation;       //可选
private String mJob;            //可选

3.1 常见的构建方式之一:定义多个重载的构造函数

public class PersonOne 
    //固定不变的对象,一般变量需要声明为 final
    private final String mName;     //必选,final 类型需要在 构造器中初始化,不允许不初始化它的构造器存在
    private String mLocation;       //可选
    private String mJob;            //可选

    public PersonOne(String name) 
        mName = name;
    

    public PersonOne(String location, String name) 
        mLocation = location;
        mName = name;
    

    public PersonOne(String name, String location, String job) 
        mName = name;
        mLocation = location;
        mJob = job;
    

优点:简单。

缺点
只适用于成员变量少的情况,太多了不容易理解、维护。不优雅。

3.2 常见的构建方式之二:使用 setter 方法挨个构造

public class PersonTwo 
    //固定不变的对象,一般变量需要声明为 final
    private final String mName;     //必选,final 类型需要在 构造器中初始化,不允许不初始化它的构造器存在
    private String mLocation;       //可选
    private String mJob;            //可选

    public PersonTwo(String s) 
         this.mName = s;
    

    public String getName() 
        return mName;
    

    public String getLocation() 
        return mLocation;
    

    public void setLocation(String location) 
        mLocation = location;
    

    public String getJob() 
        return mJob;
    

    public void setJob(String job) 
        mJob = job;
    

这种方式也是常见的构造方式,它的好处是:易于阅读,并且可以只对有用的成员变量赋值;

缺点是:

  • 成员变量不可以是 final 类型,失去了不可变对象的很多好处;
  • 对象状态不连续,你必须调用 4 次 setter 方法才能得到一个具备 4 个属性值得变量,在这期间用户可能拿到不完整状态的对象。

而且使用起来也不好看:

   PersonTwo personTwo = new PersonTwo("shixin");
   personTwo.setJob("Android");
   personTwo.setLocation("成都");

如果有 N 个属性岂不是要 personTwo.setXXX N 回?不优雅!

即使把 setXXX 方法返回值改成当前构造类,但还是不满足最重要的缺点的第二点:

用户可能拿到不完整状态的对象。

什么意思呢?

这种方式是 先创建对象、后赋值,用户不知道什么时候拿到的对象是完整的,构建完成的。很有可能你只 set 了一两个属性就返回了,一些必要的属性没有被赋值。

小结:实例化对象时尤其要注意对象的完整性处理,什么时候用builder,什么时候不用要注意区分。

3.3 优雅的构建方式:变种 Builder 模式(回到标准用法上)

为了解决上述两种构建方式,伟大的程序员们创造出了 变种 Builder 模式

经典的 Builder 模式定义为:

将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

它的重点在于:抽象出对象创建的具体步骤到一个接口,通过调用不同的接口实现,从而得到不同的结果。

Builder 模式在 Android 开发中演变出了 变种 Builder 模式,它除了具备经典构建者模式的功能,还简化了构建的过程,使得创建过程更加简单、直观。

先来看看用 变种 Builder 模式怎么实现上述 Person 对象的构建吧:

public class PersonThree 
    //固定不变的对象,一般变量需要声明为 final
    private final String mName;     //必选,final 类型需要在 构造器中初始化,不允许不初始化它的构造器存在
    private String mLocation;       //可选
    private String mJob;            //可选

    /**
     * 构造方法的参数是它的 静态内部类,使用静态内部类的变量一一赋值
     * @param builder
     */
    public PersonThree(Builder builder) 
        this.mName = builder.mName;
        this.mLocation = builder.mLocation;
        this.mJob = builder.mJob;
    

    /**
     * PersonTree 的静态内部类,成员变量和 PersonTree 的一致
     */
    public static class Builder
        private final String mName;     //必选,final 类型需要在 构造器中初始化,不允许不初始化它的构造器存在
        private String mLocation;       //可选
        private String mJob;            //可选

        /**
         * 含必选参数的构造方法
         * @param name
         */
        public Builder(String name) 
            mName = name;
        

        public Builder setLocation(String location) 
            mLocation = location;
            return this;
        

        public Builder setJob(String job) 
            mJob = job;
            return this;
        

        /**
         * 最终构建方法,返回一个 PersonTree 对象,参数是当前 Builder 对象
         * @return
         */
        public PersonThree build()
            return new PersonThree(this);
        
    

可以看到,变种 Builder 模式包括以下内容:

  • 在要构建的类内部创建一个静态内部类 Builder
  • 静态内部类的参数与构建类一致
  • 构建类的构造参数是 静态内部类,使用静态内部类的变量一一赋值给构建类
  • 静态内部类提供参数的 setter 方法,并且返回值是当前 Builder 对象
  • 最终提供一个 build 方法构建一个构建类的对象,参数是当前 Builder 对象
  new PersonThree.Builder("xiaoyangzishuo")
            .setLocation("ChengDu ")
            .setJob("Android Develop")
            .build();

变种 Builder 模式目的在于减少对象创建过程中引入的多个构造函数、可选参数以及多个 setter 过度使用导致的不必要的复杂性。

好处就是文章开头所说的:

  • 先赋值,后创建对象(注意)
  • 链式调用相对优雅

最终调用 build() 方法才创建了构建类的对象,保证了状态的完整性。

小结:这是种标准的写法,注意使用场景:有多个参数,需要区分必要参数 和可选参数,并在最后调用的方法上要保证对象的完整性。

tips:

Android Studio 中使用插件自动生成 变种 Builder 模式代码.解决了重复模板代码。

下载插件 Inner Builder 也可以快速实现Builder的构建,代码同上。略。可亲手尝试一下。

4. 链式调用(其他builder的一种变种方式)

class Calculator 
    private int total;

    public Calculator(int cardinality) 
        this.total = cardinality;
    

    public Calculator add(int addend) 
        this.total += addend;
        return this;
    

    public Calculator minus(int minus) 
        this.total -= minus;
        return this;
    

    public Calculator multiply(int multiplier) 
        this.total *= multiplier;
        return this;
    

    public Calculator divideBy(int divisor) 
        this.total /= divisor;
        return this;
    

    public Calculator calculate() 
        return this;
    

    public int getTotal() 
        return total;
    

将方法的返回值类型指定为当前 Class ,那么,在方法体的最后可以直接 return this ,从而可以形成一个类似于 Builder Patern 的效果。

用法如下:

Calculator calculator = new Calculator(5)
    .add(3)
    .minus(4)
    .multiply(2)
    .divideBy(4)
    .calculate(); //最后一行不要也能创建成功,好多文章都是说要添加,不要似乎也没有什么影响 不知你怎么看,求解惑。

小结:这种用法,不太主流,团队中使用时需要根据大家的代码使用习惯,好好评估一下功能的正确性和代码的可读性,不同的设计角度解决不同的问题,总之,没有最好,只有适合。

问题:如果需要在对象的赋值的完整性考虑,设计成sdk供业务使用方,是用构造函数来实例化对象保证完整性, 还是可以考虑用这种方式更优雅的赋值来实现,不知你会怎么选,欢迎勾兑~

5. 总结

《Effective Java》值得大家多看一看,有些经典的原则、思想可以作为开发指导原则。不同的人设计方式不同,但是总是遵循既定的经典原则。多角度思考,多运用,多反馈,最后回归再思考。

参考:

以下是从不同的使用角度对这些知识点的学习、补充。

1.《Effective Java》(经典指导原则学习)

2.《Effective Java》读书笔记02-多参构造器与Builder模式

3.变种 Builder 模式:优雅的对象构建方式

4.《effective java》builder模式一些思考

5. EffectiveJava—Builder设计模式

6. 从 Java Builder Pattern 到 return this 链式调用(了解return this的使用是怎样的)

以上是关于多参构造器与 Builder 模式及链式调用的运用的主要内容,如果未能解决你的问题,请参考以下文章

变种 Builder 模式:优雅的对象构建方式

建造者模式遇到大数据SparkSession,使链式函数编程再次流行

设计模式之建造者模式(builder)

(建造者模式)链式编程之 @Builder 和 @Accessors 学习

构造者模式(builder)

链式调用Builder