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类图。

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之后的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的关系如下。


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模式,理解会更加深刻,使用会更加得心应手。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,497评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,050评论 25 707
  • 参考Android网络请求心路历程Android Http接地气网络请求(HttpURLConnection) 一...
    合肥黑阅读 21,206评论 7 63
  • 引言 不用怀疑,你一定遇到和使用过它,它在Android源码和各种开源组件中是如此常见。这是一种你可以不知道怎么运...
    脐橙熟了阅读 947评论 0 6
  • 【青莲堂日话】160224 每日一话,是为日话 读书是件赏心悦目的事,尤其遇到心爱的,定会看不够爱不够买回家来慢慢...
    effelee阅读 182评论 1 0