Java Builder 模式,你搞懂了麼?

加油.png

前言:最近閒來無事的時候想着看看一些日常用的三方庫源碼,沒想到看了以後才知道直接擼源碼好傷身體,通常設計優秀的開源庫都會涉及不少的設計模式,就好比 android 開發使用頻繁的 okHttp 打開源碼一看,納尼?Builder 模式隨處可見,因而乎,這篇文章就來對 Builder 模式進行一個簡單總結,主要針對便於分析 android 相關源碼,以實際應用出發~

在 oop 編碼設計中,咱們有句經典的話叫作 "萬物皆對象".實際開發中,咱們只要能拿到類的實例,即對象。就能夠開始搞事情啦,能夠命令對象去作一些事情,固然啦~每一個對象的能力都是不一樣的,能作的事情也是不一樣。對象中存儲着類的成員屬性(成員變量和成員方法)。咱們命令對象去爲咱們工做,其實就是調用對象特有的屬性。剛剛咱們也說了,每一個對象的能力是不一樣的,對象所能作的事情,在一開始被建立的時候就決定了。下面先來講一下對象的構建方法。android

1、經過構造器構建

假設一個場景:咱們用一個class來表示車,車有一些必需的屬性,好比:車身,輪胎,發動機,方向盤等。也有一些可選屬性,假設超過10個,好比:車上的一些裝飾,安全氣囊等等很是多的屬性。git

若是咱們用構造器來構造對象,咱們的作法是 提供第一個包含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;
    }
}

2、JavaBeans模式構建

提供無參的構造函數,暴露一些公共的方法讓用戶本身去設置對象屬性,這種方法較之第一種彷佛加強了靈活度,用戶能夠根據本身的須要隨意去設置屬性。可是這種方法自身存在嚴重的缺點:
由於構造過程被分到了幾個調用中,在構造中 JavaBean 可能處於不一致的狀態。類沒法僅僅經過判斷構造器參數的有效性來保證一致性。還有一個嚴重的弊端是,JavaBeans 模式阻止了把類作成不可變的可能。,這就須要咱們付出額外的操做來保證它的線程安全。github

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 模式設計模式

3、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運行看看效果:ide

car默認構造.png

能夠看到,咱們默認的 car 已經制造出來了,默認的零件都是 "寶馬",滴滴滴~來不及解釋了,快上車。假如咱們不使用默認值,須要本身定製的話,很是簡單。只須要拿到 Builder 對象以後,依次調用指定方法,最後再調用 build 返回 car 便可。下面代碼示例:函數

//配置car的車身爲 奔馳
        Car car = new Car.Builder()
                .carBody("奔馳")
                .build();

依舊 debug 看看 car 是否認製成功~ oop

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個以上的話,建議使用 此模式。還有若是類屬性存在不肯定性,可能之後還會新增屬性時使用,便於擴展。

    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);
        }
    二、在 android 源碼中 AlertDialog 使用

    在 AlertDialog 中使用到的 Builder 模式也是這種套路,我相信若是前面理解了,本身去看看源碼應該是手到擒來的事。因爲篇幅緣由,在這裏就不展開了。

    結語:我的以爲 對於設計模式的學習是至關有必要的,有時候咱們須要去讀一下經常使用開源框架的源碼,不只能夠從中學習到一些設計思想,還能夠方便平常使用。在一篇博客上面看到這句話 " 咱們不重複造輪子不表示咱們不須要知道輪子該怎麼造及如何更好的造!",而設計模式即是讀懂框架源碼的基石,由於每每優秀的框架都會涉及不少設計模式。後面本人也會不斷更新,不斷學習新的設計模式,進而總結出來~

    聲明:以上僅僅是本人的一點拙見,若有不足之處,還望指出

    更多原創文章會在公衆號第一時間推送,歡迎掃碼關注 張少林同窗

    張少林同窗.jpg

    相關文章
    相關標籤/搜索