Builder 模式演义 (2)——OkHttp 源码中的 Builder 模式

引言

  在上一篇Builder模式演义(1)中介绍了Builder模式的标准形式,以及两种基本变换——链式调用和省略指挥者角色。本文将通过分析OkHttp源码阐述Builder模式的另外两种变换——省略抽象Builder角色和Product角色回炉再造。

OkHttp源码中的Builder模式

  OkHttp作为开源的Android网络请求框架,以URLConnection和HttpClient的替代者身份出现,名噪江湖。许多开源框架都是基于OkHttp的二次封装,比如OkGo,以及与OkHttp同源的Retrofit。OkHttp的使用非常简单,在Github上OkHttp项目的Wiki/Recipes中有基本的介绍。

private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://publicobject.com/helloworld.txt")
        .build();
    Response response = client.newCall(request).execute();
//省略其他代码
}

  由上述代码的调用风格我们可以基本猜到,Request对象的构建采用了Builder模式。为了验证这一点,我们看看源码中Request类的具体实现。

public final class Request {
  final HttpUrl url;
  final String method;
  final Headers headers;
  final RequestBody body;
  final Object tag;

  private volatile CacheControl cacheControl; // Lazily initialized.

  Request(Builder builder) {
    //构造函数,省略属性赋值操作
  }

  public Builder newBuilder() {
    return new Builder(this);
  }
//省略部分代码
  public static class Builder {
    HttpUrl url;
    String method;
    Headers.Builder headers;
    RequestBody body;
    Object tag;
//省略部分代码
    Builder(Request request) {
      //构造函数,省略属性赋值操作
    }
//省略部分代码
     public Request build() {
      if (url == null) throw new IllegalStateException("url == null");
         return new Request(this);
    }
}

  上述代码非常清晰,Request类共有6个属性,从名字就可以猜出它们的意义:url代表这个Http请求的url,method指的是POST请求还是GET请求还是其他,header和body自然就是http请求的首部和请求体;tag猜测是用作取消一个请求,cacheControl是缓存控制。Request.Builder中冗余定义了这6个属性。builder在经过一系列的链式调用,对这6个属性中的某几个进行赋值后,最终调用build()方法,生成一个完整的Request对象。build()方法的具体实现也很简单,调用Request的构造方法,将自己作为参数传递过去。Request构造方法内对6个属性一一赋值(没有值时使用默认值)。

省略抽象Builder角色

  仔细研究,发现Request类的6个属性分别对应的类型中,Headers、CacheControl、HttpUrl类的内部都含有Builder!RequestBody是个抽象类,其内部没有Builder,但是它的两个子类FormBody和MultipartBody有。于是画出如下UML类图。

《Builder 模式演义 (2)——OkHttp 源码中的 Builder 模式》

OkHttp源码中使用Builder模式的部分类

  对照上一篇Builder模式演义(1)中GoF标准Builder模式,原本我们很希望RequestBody中有一个Builder作为抽象Builder角色,作为FormBody.Builder和MultipartBody.Builder的共同父类。然而Request.Builder根本不存在!上图中除了RequestBody,其他每个类中的Builder都是独立存在的,除宿主类(Builder所在的外部类),不和其他类发生任何牵扯。
  换一种方式理解,如果Builder模式中的ConcreteBuilder只有一个,那么抽象的Builder当然可以省略。此所谓Builder模式变换之省略抽象Builder角色

Product角色回炉再造

  省略指挥者角色,省略抽象Builder角色,整个Builder模式只剩下两个角色,如下图。

《Builder 模式演义 (2)——OkHttp 源码中的 Builder 模式》

省略指挥者和抽象Builder之后的Builder模式

  相信你已经非常熟悉Builder模式的使用套路了——在经过一系列的链式调用对属性进行赋值后,ConcreteBuilder最终调用build()方法生成Product对象。一旦调用build()方法,无法再设置或修改属性值了,因为build()返回的是Product类型,而不再是Builder本身。这本身是一种保护机制,也是Builder模式的特性。这好比打包邮寄东西,一旦封包,无法再继续往里面塞,更无法在运输的途中,进行远程遥控替换里面的某件物品。
  然而凡事无绝对,设想这样一种场景:假如上述的Request类中的属性数不是6而是30,通过长长的链式调用,我配置了其中的20个属性,一声令下调用build()方法获取到了一个request1对象,并一直在使用着;在某个特殊的场景下,我需要使用和request1基本相同的配置,只有两个属性值不同。这时候该如何去获得request1对象的一个拷贝,然后设置那两个不同的属性值呢?想到两种方式:

  1. 让Request实现Cloneable接口,或者仿照C++实现一个形如Request(Request other){…}的拷贝构造函数。
  2. 将request1对象序列化后再反序列化,得到另一个对象request2,它和request1所有属性都相同。

  方式1存在着深拷贝、浅拷贝的问题,再者,为每个复杂对象实现Clonable接口或拷贝构造函数工作量巨大而且非常难以维护;方式2存在着空间和性能的开销。Builder模式的问题,有它自己的解决逻辑!从Builder到Product并不一定是单向不可逆的过程。回看文章开头Request类的源码,有一个newBuilder()方法,它返回Request.Builder()类型。newBuilder()的内部,调用的是Builder的一个含Request类型参数的构造函数。

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

  Request(Builder builder){…}和Builder(Request request) {…},外部类和内部类的构造函数,是否有种对称美?正是这种美,巧妙地完成了从Product重回Builder的逆向过程。再接下来的事,就是继续链式调用最后调用build()模式一锤定音。至此,Builder模式Builder和Product的关系如下。

《Builder 模式演义 (2)——OkHttp 源码中的 Builder 模式》

Product和Builder角色的相互转换

OkHttp官网解释回炉再造

  其实在Github上OkHttp项目的Wiki/Recipes中,有这样一段话:

Per-call Configuration
All the HTTP client configuration lives in OkHttpClient
including proxy settings, timeouts, and caches. When you need to change the configuration of a single call, call OkHttpClient.newBuilder()
. This returns a builder that shares the same connection pool, dispatcher, and configuration with the original client. In the example below, we make one request with a 500 ms timeout and another with a 3000 ms timeout.

  大体意思是,所有HTTP请求都使用全局的OKHttpClient配置,包括代理、超时、缓存等。如果要为某一两个单独的请求修改配置,就调用OkHttpClient.newBuilder()。它返回的OkHttpClient.Builder和原先那个全局的有一样的连接池、分发器和配置。然后就是示例代码,如下。

private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {     Request request = new Request.Builder()
        .url("http://httpbin.org/delay/1") // This URL is served with a 1 second delay.
        .build();

    try {
      // Copy to customize OkHttp for this request.       OkHttpClient copy = client.newBuilder()           .readTimeout(500, TimeUnit.MILLISECONDS)
          .build();

      Response response = copy.newCall(request).execute();       System.out.println("Response 1 succeeded: " + response);
    } catch (IOException e) {
      System.out.println("Response 1 failed: " + e);
    }

    try {
      // Copy to customize OkHttp for this request.       OkHttpClient copy = client.newBuilder()           .readTimeout(3000, TimeUnit.MILLISECONDS)
          .build();

      Response response = copy.newCall(request).execute();       System.out.println("Response 2 succeeded: " + response);
    } catch (IOException e) {
      System.out.println("Response 2 failed: " + e);
    }
  }

  对,你没有看错,这不是Request.newBuilder()的使用,而是OKHttpClient.newBuilder()。OKHttpClient类也使用Builer模式!具体的实现请自行查看源码了。

总结

  至此,已经介绍了Builder模式的四种变换。

  1. 链式调用:
      并非Builder模式特有,只要在原本返回值为void的方法中返回this,都可以实现链式调用。
  2. 省略指挥者角色:
      new builder、链式赋值、最后build,一条龙调用,不再需要指挥者角色。
  3. 省略抽象Builder角色:
      具体的Builder只有一个,省略抽象父类。
  4. Product角色的回炉再造:
      Product逆转化为Builder,调整某些配置后,重新build,回到Product形态。

  Builder设计模式使用如此广泛,又如此灵活。我们在实际开发特别是重构、封装时,可适当借鉴,定能更上一层逼格。有时间可以再挖一挖OkGo和Retrofit中的Builder模式,理解会更加深刻,使用会更加得心应手。

    原文作者:算法小白
    原文地址: https://juejin.im/entry/58cb47fb5c497d0057b4da24
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞