Java Builder 模式,你搞懂了么?

·  阅读 17665

加油.png

前言:最近闲来无事的时候想着看看一些平常用的三方库源码,没想到看了之后才知道直接撸源码好伤身体,一般设计优秀的开源库都会涉及很多的设计模式,就比如 android 开发使用频繁的 okHttp 打开源码一看,纳尼?Builder 模式随处可见,于是乎,这篇文章就来对 Builder 模式进行一个简单总结,主要针对便于分析 android 相关源码,以实际应用出发~

在 oop 编码设计中,我们有句经典的话叫做 "万物皆对象".实际开发中,我们只要能拿到类的实例,即对象。就可以开始搞事情啦,可以命令对象去做一些事情,当然啦~每个对象的能力都是不同的,能做的事情也是不同。对象中存储着类的成员属性(成员变量和成员方法)。我们命令对象去为我们工作,其实就是调用对象特有的属性。刚刚我们也说了,每个对象的能力是不同的,对象所能做的事情,在一开始被创建的时候就决定了。下面先来说一下对象的构建方法。

一、通过构造器构建

假设一个场景:我们用一个class来表示车,车有一些必需的属性,比如:车身,轮胎,发动机,方向盘等。也有一些可选属性,假设超过10个,比如:车上的一些装饰,安全气囊等等非常多的属性。

如果我们用构造器来构造对象,我们的做法是 提供第一个包含4个必需属性的构造器,接下来再按可选属性依次重载不同的构造器,这样是可行的,但是会有以下一些问题:

  • 一旦属性非常多,需要重载n多个构造器,而且各种构造器的组成都是在特定需求的情况下制定的,代码量多了不说,灵活性大大下降
  • 客户端调用构造器的时候,需要传的属性非常多,可能导致调用困难,我们需要去熟悉每个特定构造器所提供的属性是什么样的,而参数属性多的情况下,我们可能因为疏忽而传错顺序。
public class Car {
    /**
     * 必需属性
     */
    private String carBody;//车身
    private String tyre;//轮胎
    private String engine;//发动机
    private String aimingCircle;//方向盘
    /**
     * 可选属性
     */
    private String decoration;//车内装饰品

    /**
     * 必需属性构造器
     *
     * @param carBody
     * @param tyre
     * @param engine
     */
    public Car(String carBody, String tyre, String engine) {
        this.carBody = carBody;
        this.tyre = tyre;
        this.engine = engine;
    }

    /**
     * 假如我们需要再添加车内装饰品,即在原来构造器基础上再重载一个构造器
     *
     * @param carBody
     * @param tyre
     * @param engine
     * @param aimingCircle
     * @param decoration
     */
    public Car(String carBody, String tyre, String engine, String aimingCircle, String decoration) {
        this.carBody = carBody;
        this.tyre = tyre;
        this.engine = engine;
        this.aimingCircle = aimingCircle;
        this.decoration = decoration;
    }
}
复制代码

二、JavaBeans模式构建

提供无参的构造函数,暴露一些公共的方法让用户自己去设置对象属性,这种方法较之第一种似乎增强了灵活度,用户可以根据自己的需要随意去设置属性。但是这种方法自身存在严重的缺点: 因为构造过程被分到了几个调用中,在构造中 JavaBean 可能处于不一致的状态。类无法仅仅通过判断构造器参数的有效性来保证一致性。还有一个严重的弊端是,JavaBeans 模式阻止了把类做成不可变的可能。,这就需要我们付出额外的操作来保证它的线程安全。

public class Car {
    /**
     * 必需属性
     */
    private String carBody;//车身
    private String tyre;//轮胎
    private String engine;//发动机
    private String aimingCircle;//方向盘
    /**
     * 可选属性
     */
    private String decoration;//车内装饰品

    public void setCarBody(String carBody) {
        this.carBody = carBody;
    }

    public void setTyre(String tyre) {
        this.tyre = tyre;
    }

    public void setEngine(String engine) {
        this.engine = engine;
    }

    public void setAimingCircle(String aimingCircle) {
        this.aimingCircle = aimingCircle;
    }

    public void setDecoration(String decoration) {
        this.decoration = decoration;
    }
}
复制代码

那么有没有什么方法可以解决以上问题呢?当然有啦~下面我们的主角上场-----Builder 模式

三、Builder 模式

我们用户一般不会自己来完成 car 组装这些繁琐的过程,而是把它交给汽车制造商。由汽车制造商去完成汽车的组装过程,这里的 Builder 就是汽车制造商,我们的 car 的创建都交由他来完成,我们只管开车就是啦, 先来个代码实际体验一下~

public final class Car {
    /**
     * 必需属性
     */
    final String carBody;//车身
    final String tyre;//轮胎
    final String engine;//发动机
    final String aimingCircle;//方向盘
    final String safetyBelt;//安全带
    /**
     * 可选属性
     */
    final String decoration;//车内装饰品
    /**
     * car 的构造器 持有 Builder,将builder制造的组件赋值给 car 完成构建
     * @param builder
     */
    public Car(Builder builder) {
        this.carBody = builder.carBody;
        this.tyre = builder.tyre;
        this.engine = builder.engine;
        this.aimingCircle = builder.aimingCircle;
        this.decoration = builder.decoration;
        this.safetyBelt = builder.safetyBelt;
    }
    ...省略一些get方法
    public static final class Builder {
        String carBody;
        String tyre;
        String engine;
        String aimingCircle;
        String decoration;
        String safetyBelt;

        public Builder() {
            this.carBody = "宝马";
            this.tyre = "宝马";
            this.engine = "宝马";
            this.aimingCircle = "宝马";
            this.decoration = "宝马";
        }
         /**
         * 实际属性配置方法
         * @param carBody
         * @return
         */
        public Builder carBody(String carBody) {
            this.carBody = carBody;
            return this;
        }

        public Builder tyre(String tyre) {
            this.tyre = tyre;
            return this;
        }
        public Builder safetyBelt(String safetyBelt) {
          if (safetyBelt == null) throw new NullPointerException("没系安全带,你开个毛车啊");
            this.safetyBelt = safetyBelt;
            return this;
        }
        public Builder engine(String engine) {
            this.engine = engine;
            return this;
        }

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

        public Builder decoration(String decoration) {
            this.decoration = decoration;
            return this;
        }
        /**
         * 最后创造出实体car
         * @return
         */
        public Car build() {
            return new Car(this);
        }
    }
}
复制代码

现在我们的类就写好了,我们调用的时候执行一下代码:

 Car car = new Car.Builder()
                .build();
复制代码

打断点,debug运行看看效果:

car默认构造.png

可以看到,我们默认的 car 已经制造出来了,默认的零件都是 "宝马",滴滴滴~来不及解释了,快上车。假如我们不使用默认值,需要自己定制的话,非常简单。只需要拿到 Builder 对象之后,依次调用指定方法,最后再调用 build 返回 car 即可。下面代码示例:

        //配置car的车身为 奔驰
        Car car = new Car.Builder()
                .carBody("奔驰")
                .build();
复制代码

依旧 debug 看看 car 是否定制成功~

car 定制.png

咦,神奇的定制 car 定制成功了,话不多说,继续开车~~

我们在 Builder 类中的一系列构建方法中还可以加入一些我们对配置属性的限制。例如我们给 car 添加一个安全带属性,在 Buidler 对应方法出添加以下代码:

 public Builder safetyBelt(String safetyBelt) {
            if (safetyBelt == null) throw new NullPointerException("没系安全带,你开个毛车啊");
            this.safetyBelt = safetyBelt;
            return this;
        }
复制代码

然后调用的时候:

     //配置car的车身为 奔驰
     Car car = new Car.Builder()
                      .carBody("奔驰")
                      .safetyBelt(null)
                      .build();
复制代码

我们给配置安全带属性加了 null 判断,一但配置了null 属性,即会抛出异常。好了 car 构建好了,我们来开车看看~

依旧 debug 开车走起~

car 属性配置判断.png

bom~~~不出意外,翻车了。。。

最后有客户说了,你制造出来的 car 体验不是很好,想把车再改造改造,可是车已经出厂了还能改造吗?那这应该怎么办呢?不要急,好说好说,我们只要能再拿到 Builder 对象就有办法。下面我们给 Builder 添加如下构造,再对比下 Car 的构造看看有啥奇特之处:

       /**
         * 回厂重造
         * @param car
         */
        public Builder(Car car) {
            this.carBody = car.carBody;
            this.safetyBelt = car.safetyBelt;
            this.decoration = car.decoration;
            this.tyre = car.tyre;
            this.aimingCircle = car.aimingCircle;
            this.engine = car.engine;
        }
  /**
     * car 的构造器 持有 Builder,将 builder 制造的组件赋值给 car 完成构建
     *
     * @param builder
     */
    public Car(Builder builder) {
        this.carBody = builder.carBody;
        this.tyre = builder.tyre;
        this.engine = builder.engine;
        this.aimingCircle = builder.aimingCircle;
        this.decoration = builder.decoration;
        this.safetyBelt = builder.safetyBelt;
    }

复制代码

咦,似乎有着对称的关系,没错。我们提供对应的构造。调用返回对应的对象,可以实现返回的效果。在 Car 中添加方法

 /**
     * 重新拿回builder 去改造car
     * @return
     */
    public Builder newBuilder() {
        return new Builder(this);
    }
复制代码

现在来试试能不能返厂重建?把原来的宝马车重造成奔驰车,调用代码:

Car newCar = car.newBuilder()
                .carBody("奔驰")
                .safetyBelt("奔驰")
                .tyre("奔驰")
                .aimingCircle("奔驰")
                .decoration("奔驰")
                .engine("奔驰")
                .build();
复制代码

行,车改造好了,我们继续 debug ,试试改造完满不满意

car 改造.png
哈哈,已经改造好了,客户相当满意~~

下面分析一下具体是怎么构建的。

  • 新建静态内部类 Builder ,也就是汽车制造商,我们的 car 交给他来制造,car 需要的属性 全部复制进来
  • 定义 Builder 空构造,初始化 car 默认值。这里是为了初始化构造的时候,不要再去特别定义属性,直接使用默认值。定义 Builder 构造,传入 Car ,构造里面执行 Car 属性赋值 给 Builder 对应属性的操作,目的是为了重建一个builder 进行返厂重造
  • 定义一系列方法进行属性初始化,这些方法跟 JavaBeans 模式构建 中的方法类似,不同的是,返回值为 Builder 类型,为了方便链式调用。最后定义方法返回实体 Car 对象,car 的构造器 持有 Builder,最终将builder制造的组件赋值给 car 完成构建

至此,我们的 Builder 模式体验就结束了,这里讲的只是 Builder 模式的一个变种,即在 android 中应用较为广泛的模式,下面总结一下优缺点:

优点

  • 解耦,逻辑清晰。统一交由 Builder 类构造,Car 类不用关心内部实现细节,只注重结果。

  • 链式调用,使用灵活,易于扩展。相对于方法一中的构造器方法,配置对象属性灵活度大大提高,支持链式调用使得逻辑清晰不少,而且我们需要扩展的时候,也只需要添加对应扩展属性即可,十分方便。

缺点

  • 硬要说缺点的话 就是前期需要编写更多的代码,每次构建需要先创建对应的 Builder 对象。但是这点开销几乎可以忽略吧,前期编写更多的代码是为了以后更好的扩展,这不是优秀程序员应该要考虑的事么

解决方法: 不会偷懒的程序猿不是好程序猿,针对以上缺点,IDEA 系列的 ide ,有相应的插件 InnerBuilder 可以自动生成 builder 相关代码,安装自行 google,使用的时候只需要在实体类中 alt + insert 键,会有个 build 按钮提供代码生成。

使用场景 一般如果类属性在4个以上的话,建议使用 此模式。还有如果类属性存在不确定性,可能以后还会新增属性时使用,便于扩展。

四、Builder 模式在 android 中的应用

1. 在 okHttp 中广泛使用

开篇我们也说到了 Builder 模式在 okHttp 中随处可见。比如在OkHttpClient,Request,Response 等类都使用了此模式。下面以 Request 类为例简要说明,具体的可以去下载源码查看,按照上面的套路基本没问题。

Request 有6个属性,按照套路 构造方法持有一个 Builder ,在构造中将 builder 制造的组件赋值给 Request 完成构建,提供 newBuilder 用于重新获得 Builder 返厂重建:

final HttpUrl url;
  final String method;
  final Headers headers;
  final RequestBody body;
  final Object tag;

  private volatile CacheControl cacheControl; // Lazily initialized.

  Request(Builder builder) {
    this.url = builder.url;
    this.method = builder.method;
    this.headers = builder.headers.build();
    this.body = builder.body;
    this.tag = builder.tag != null ? builder.tag : this;
  }

  public Builder newBuilder() {
    return new Builder(this);
  }

复制代码

Builder 有两个构造,第一个空构造中初始化两个默认值。第二个构造持有 Request 用于重新构建 Builder 返厂重建。

public Builder() {
      this.method = "GET";
      this.headers = new Headers.Builder();
    }

    Builder(Request request) {
      this.url = request.url;
      this.method = request.method;
      this.body = request.body;
      this.tag = request.tag;
      this.headers = request.headers.newBuilder();
    }
复制代码

剩下的就是一些属性初始化的方法,返回值为 Builder 方便链式调用。这里就列出一个方法,详细的请查看源码,最后调用 build() 方法 初始化 Request 传入 Builder 完成构建。

  public Builder url(HttpUrl url) {
      if (url == null) throw new NullPointerException("url == null");
      this.url = url;
      return this;
    }
...此处省略部分方法
  public Request build() {
      if (url == null) throw new IllegalStateException("url == null");
      return new Request(this);
    }
复制代码
2、在 android 源码中 AlertDialog 使用

在 AlertDialog 中使用到的 Builder 模式也是这种套路,我相信如果前面理解了,自己去看看源码应该是手到擒来的事。由于篇幅原因,在这里就不展开了。

结语:个人觉得 对于设计模式的学习是相当有必要的,有时候我们需要去读一下常用开源框架的源码,不仅可以从中学习到一些设计思想,还可以方便日常使用。在一篇博客上面看到这句话 " 我们不重复造轮子不表示我们不需要知道轮子该怎么造及如何更好的造!",而设计模式便是读懂框架源码的基石,因为往往优秀的框架都会涉及很多设计模式。后面本人也会不断更新,不断学习新的设计模式,进而总结出来~

声明:以上仅仅是本人的一点拙见,如有不足之处,还望指出

更多原创文章会在公众号第一时间推送,欢迎扫码关注 张少林同学

张少林同学.jpg

分类:
后端
标签:
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改