变种 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 条评论
登录 后参与评论

相关文章

来自专栏Kubernetes

containerd源码分析

本文是对containerd v0.2.4的源码分析。 ##Containerd源码流程图 ? 源码接口调用详情 从ctr调用containerd-api #...

4707
来自专栏不想当开发的产品不是好测试

java 执行sql文件

# 背景 用例执行完毕,期望回滚数据,因此希望执行sql来回滚数据 # 步骤 直接show代码,借助的是mybatis的ScriptRunner /** ...

3228
来自专栏一个会写诗的程序员的博客

并发编程之CAS(Compare and Swap)原理Unsafe类

AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现...

771
来自专栏java、Spring、技术分享

Feign与Spring Cloud源码解析

  在Feign的官方文档上, 我们可以看到Feign最重要的一句话是:Feign makes writing java http clients easier...

982
来自专栏CodingToDie

百篇(5):FeignClient 在不同场景中的应用

FeignClient 使用 为了测试方便,这里提供四个项目 user-server user-server-api spring-boot-feign spr...

1.2K5
来自专栏CodingToDie

传统Spring项目使用FeignClient组件访问微服务

传统Spring项目使用 这里的传统 Spring项目指的是没有使用 spring boot的 spring项目,例如 ssm api 文件 和在spring ...

2K8
来自专栏Java架构师学习

带你深入了解Java线程中的那些事

引言 说到Thread大家都很熟悉,我们平常写并发代码的时候都会接触到,那么我们来看看下面这段代码是如何初始化以及执行的呢? public class Thre...

2818
来自专栏何俊林

插件开发之360 DroidPlugin源码分析(五)Service预注册占坑

在了解系统的activity,service,broadcastReceiver的启动过程后,今天将分析下360 DroidPlugin是如何预注册占坑的?本篇...

1926
来自专栏Java架构沉思录

优雅实现延时任务之Redis篇

延时任务,顾名思义,就是延迟一段时间后才执行的任务。举个例子,假设我们有个发布资讯的功能,运营需要在每天早上7点准时发布资讯,但是早上7点大家都还没上班,这个时...

843
来自专栏Code_iOS

数据结构:集合

工程代码 Github: Data_Structures_C_Implemention -- Set

933

扫码关注云+社区