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

帅气的 Builder 链式调用

在日常开发中,经常可以看到这样的代码:

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

或者

new AlertDialog.Builder(this)
            .setTitle("hello")
            .setMessage("I'm shixinzhang")
            .setIcon(R.drawable.bg_search_corner)
            .setCancelable(true)
            .setOnCancelListener(new DialogInterface.OnCancelListener() {
                @Override
                public void onCancel(DialogInterface dialog) {
                    //...
                }
            })
            .show();

可以看到这样链式调用看起来好整齐啊,Builder 模式早有耳闻,今天就来详细了解一下。

常见的两种构建方式

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

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

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

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

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

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

    public PersonOne(String name, String location, String job, String habit) {
        mName = name;
        mLocation = location;
        mJob = job;
        mHabit = habit;
    }
}

这种方式的优点:简单。

看起来很简单嘛,每个构造函数都需要啥参数,第几个参数是干什么的,看一下就知道了,嗯,很直观!

等等!这是你站在开发者的角度的结果!

使用者使用时可得仔细了解你每个构造函数,否则一不小心填错顺序也不知道。

而且如果有十几个属性,我靠,你见过有十几个参数的构造函数吗?

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

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

吸取上面的教训,我不在构造方法里穿参数了,改成用 set 方法挨个构造,可以了吧。

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

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

    public String getHabit() {
        return mHabit;
    }

    public void setHabit(String habit) {
        mHabit = habit;
    }
}

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

缺点是:

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

而且使用起来也不好看:

    PersonTwo personTwo = new PersonTwo("shixin");
    personTwo.setJob("洗剪吹");
    personTwo.setLocation("温州");
    personTwo.setHabit("嘿嘿嘿");

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

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

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

什么意思呢?

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

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

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

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

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

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

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

        /**
         * 含必选参数的构造方法
         * @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;
        }

        public Builder setHabit(String habit) {
            mHabit = habit;
            return this;
        }

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

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

  • 在要构建的类内部创建一个静态内部类 Builder
  • 静态内部类的参数与构建类一致
  • 构建类的构造参数是 静态内部类,使用静态内部类的变量一一赋值给构建类
  • 静态内部类提供参数的 setter 方法,并且返回值是当前 Builder 对象
  • 最终提供一个 build 方法构建一个构建类的对象,参数是当前 Builder 对象

调用代码:

    new PersonThree.Builder("shixinzhang")
            .setLocation("Shanghai")
            .setJob("Android Develop")
            .setHabit("LOL")
            .build();

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

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

  • 看起来很整齐;
  • 先赋值,后创建对象。

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

缺点嘛,就是需要额外写的代码多了点。

不过还好,有前辈开发出了 Android Studio 插件来拯救我们。

Android Studio 中使用插件自动生成 变种 Builder 模式代码

第一步:下载插件 Inner Builder:

第二步:重启 Andriod Studio;

第三步:写好要构建的类的变量:

比如:

public class PersonTest {
    private final String mName;
    private int mAge;
    private String mLocation;

}

第四步:按 Control + Insert (Mac :command + N):

第五步:在弹出的 Generate 对话框中选择 Builder:

第六步:选中要使用 Builder 构建的对象,然后勾选使用的配置,点击 OK :

public class PersonTest {
    private final String mName;
    private int mAge;
    private String mLocation;

    private PersonTest(Builder builder) {
        mName = builder.mName;
        mAge = builder.mAge;
        mLocation = builder.mLocation;
    }

    public static final class Builder {
        private String mName;
        private int mAge;
        private String mLocation;

        public Builder() {
        }

        public Builder mName(String mName) {
            this.mName = mName;
            return this;
        }

        public Builder mAge(int mAge) {
            this.mAge = mAge;
            return this;
        }

        public Builder mLocation(String mLocation) {
            this.mLocation = mLocation;
            return this;
        }

        public PersonTest build() {
            return new PersonTest(this);
        }
    }
}

基本上和我们手写的一致,是不是方便很多呢?

总结

经典的 Builder 模式定义为:

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

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

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

Thanks

《Android 高级进阶》这本书的目录值得一看

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏帮你学MatLab

《Experiment with MATLAB》读书笔记(三)

读书笔记(三) 这是第三部分日期函数 将代码复制到m文件即可运行 函数部分需新建m文件保存 %% 获取当前时间 format bank % ...

29110
来自专栏IMWeb前端团队

当JavaScript遇上UINT64

导语:写下这篇文章的缘由是因为在项目过程中,碰到了一个使用JavaScript处理 UINT64 类型数字的坑。 与大部分现代编程语言(包括几乎所有的脚本语言...

2560
来自专栏逍遥剑客的游戏开发

在C#中派生C++的抽象类

2074
来自专栏進无尽的文章

编码篇-低耦合代码注入

我下面要将的内容也许网上已经有很多相关的介绍了,但是我还是会写出这篇文章,一来是对自己学习的总结,虽然总结的有些晚,如果你仔细看,会发现我的文章有别处没有的内容...

942
来自专栏林德熙的博客

C# 序列类为 xml 可以使用的特性大全

本文告诉大家如何使用序列类,以及序列时可以用到的特性,特性的作用和一些容易被问的问题

1181
来自专栏向治洪

Android开发优化之——使用软引用和弱引用

Java从JDK1.2版本开始,就把对象的引用分为四种级别,从而使程序能更加灵活的控制对象的生命周期。这四种级别由高到低依次为:强引用、软引用、弱引用和虚引用...

1849
来自专栏移动端开发

Swift Runtime ?

你肯定也想过       在OC中相信每一个iOS开发都知道Runtime, 现在Swift也更新到4.0版本了,要是你也学习过Swift的话你可能也会想过这样...

8727
来自专栏博客园

X--名称空间详解

X名称空间里面的成员(如X:Name,X:Class)都是写给XAML编译器看的、用来引导XAML代码将XAML代码编译为CLR代码。

1422
来自专栏逍遥剑客的游戏开发

Nebula3学习笔记(3): 核心库

18811
来自专栏程序员的SOD蜜

在C++中反射调用.NET(一) 反射调用第一个.NET类的方法

为什么要在C++中调用.NET 一般情况下,我们常常会在.NET程序中调用C/C++的程序,使用P/Invoke方式进行调用,在编写代码代码的时候,首先要导入D...

24110

扫码关注云+社区