Java Builder 模式,你搞懂了麼?

張少林同學發表於2018-12-24

加油.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

相關文章