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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章