Java Builder 模式,你搞懂了么?


加油.png加油.png

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

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

一、通過構造器構建

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

如果我們用構造器來構造對象,我們的做法是 提供第一個包含4個必需屬性的構造器,接下來再按可選屬性依次重載不同的構造器,這樣是可行的,但是會有以下一些問題:

  • 一旦屬性非常多,需要重載n多個構造器,而且各種構造器的組成都是在特定需求的情況下制定的,代碼量多了不說,靈活性大大下降
  • 客戶端調用構造器的時候,需要傳的屬性非常多,可能導致調用困難,我們需要去熟悉每個特定構造器所提供的屬性是什么樣的,而參數屬性多的情況下,我們可能因為疏忽而傳錯順序。
 1public class Car {
2    /**
3     * 必需屬性
4     */

5    private String carBody;//車身
6    private String tyre;//輪胎
7    private String engine;//發動機
8    private String aimingCircle;//方向盤
9    /**
10     * 可選屬性
11     */

12    private String decoration;//車內裝飾品
13
14    /**
15     * 必需屬性構造器
16     *
17     * @param carBody
18     * @param tyre
19     * @param engine
20     */

21    public Car(String carBody, String tyre, String engine) {
22        this.carBody = carBody;
23        this.tyre = tyre;
24        this.engine = engine;
25    }
26
27    /**
28     * 假如我們需要再添加車內裝飾品,即在原來構造器基礎上再重載一個構造器
29     *
30     * @param carBody
31     * @param tyre
32     * @param engine
33     * @param aimingCircle
34     * @param decoration
35     */

36    public Car(String carBody, String tyre, String engine, String aimingCircle, String decoration) {
37        this.carBody = carBody;
38        this.tyre = tyre;
39        this.engine = engine;
40        this.aimingCircle = aimingCircle;
41        this.decoration = decoration;
42    }
43}

二、JavaBeans模式構建

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

 1public class Car {
2    /**
3     * 必需屬性
4     */

5    private String carBody;//車身
6    private String tyre;//輪胎
7    private String engine;//發動機
8    private String aimingCircle;//方向盤
9    /**
10     * 可選屬性
11     */

12    private String decoration;//車內裝飾品
13
14    public void setCarBody(String carBody) {
15        this.carBody = carBody;
16    }
17
18    public void setTyre(String tyre) {
19        this.tyre = tyre;
20    }
21
22    public void setEngine(String engine) {
23        this.engine = engine;
24    }
25
26    public void setAimingCircle(String aimingCircle) {
27        this.aimingCircle = aimingCircle;
28    }
29
30    public void setDecoration(String decoration) {
31        this.decoration = decoration;
32    }
33}

那么有沒有什么方法可以解決以上問題呢?當然有啦~下面我們的主角上場-----Builder 模式

三、Builder 模式

我們用戶一般不會自己來完成 car 組裝這些繁瑣的過程,而是把它交給汽車制造商。由汽車制造商去完成汽車的組裝過程,這里的 Builder 就是汽車制造商,我們的 car 的創建都交由他來完成,我們只管開車就是啦, 先來個代碼實際體驗一下~

 1public final class Car {
2    /**
3     * 必需屬性
4     */

5    final String carBody;//車身
6    final String tyre;//輪胎
7    final String engine;//發動機
8    final String aimingCircle;//方向盤
9    final String safetyBelt;//安全帶
10    /**
11     * 可選屬性
12     */

13    final String decoration;//車內裝飾品
14    /**
15     * car 的構造器 持有 Builder,將builder制造的組件賦值給 car 完成構建
16     * @param builder
17     */

18    public Car(Builder builder) {
19        this.carBody = builder.carBody;
20        this.tyre = builder.tyre;
21        this.engine = builder.engine;
22        this.aimingCircle = builder.aimingCircle;
23        this.decoration = builder.decoration;
24        this.safetyBelt = builder.safetyBelt;
25    }
26    ...省略一些get方法
27    public static final class Builder {
28        String carBody;
29        String tyre;
30        String engine;
31        String aimingCircle;
32        String decoration;
33        String safetyBelt;
34
35        public Builder() {
36            this.carBody = "寶馬";
37            this.tyre = "寶馬";
38            this.engine = "寶馬";
39            this.aimingCircle = "寶馬";
40            this.decoration = "寶馬";
41        }
42         /**
43         * 實際屬性配置方法
44         * @param carBody
45         * @return
46         */

47        public Builder carBody(String carBody) {
48            this.carBody = carBody;
49            return this;
50        }
51
52        public Builder tyre(String tyre) {
53            this.tyre = tyre;
54            return this;
55        }
56        public Builder safetyBelt(String safetyBelt) {
57          if (safetyBelt == nullthrow new NullPointerException("沒系安全帶,你開個毛車啊");
58            this.safetyBelt = safetyBelt;
59            return this;
60        }
61        public Builder engine(String engine) {
62            this.engine = engine;
63            return this;
64        }
65
66        public Builder aimingCircle(String aimingCircle) {
67            this.aimingCircle = aimingCircle;
68            return this;
69        }
70
71        public Builder decoration(String decoration) {
72            this.decoration = decoration;
73            return this;
74        }
75        /**
76         * 最后創造出實體car
77         * @return
78         */

79        public Car build() {
80            return new Car(this);
81        }
82    }
83}

現在我們的類就寫好了,我們調用的時候執行一下代碼:

1 Car car = new Car.Builder()
2                .build();

打斷點,debug運行看看效果:

car默認構造.pngcar默認構造.png

可以看到,我們默認的 car 已經制造出來了,默認的零件都是 "寶馬",滴滴滴~來不及解釋了,快上車。假如我們不使用默認值,需要自己定制的話,非常簡單。只需要拿到 Builder 對象之后,依次調用指定方法,最后再調用 build 返回 car 即可。下面代碼示例:

1        //配置car的車身為 奔馳
2        Car car = new Car.Builder()
3                .carBody("奔馳")
4                .build();

依舊 debug 看看 car 是否定制成功~

car 定制.pngcar 定制.png

咦,神奇的定制 car 定制成功了,話不多說,繼續開車~~

我們在 Builder 類中的一系列構建方法中還可以加入一些我們對配置屬性的限制。例如我們給 car 添加一個安全帶屬性,在 Buidler 對應方法出添加以下代碼:

1 public Builder safetyBelt(String safetyBelt) {
2            if (safetyBelt == nullthrow new NullPointerException("沒系安全帶,你開個毛車啊");
3            this.safetyBelt = safetyBelt;
4            return this;
5        }

然后調用的時候:

1     //配置car的車身為 奔馳
2     Car car = new Car.Builder()
3                      .carBody("奔馳")
4                      .safetyBelt(null)
5                      .build();

我們給配置安全帶屬性加了 null 判斷,一但配置了null 屬性,即會拋出異常。好了 car 構建好了,我們來開車看看~

依舊 debug 開車走起~

car 屬性配置判斷.pngcar 屬性配置判斷.png

bom~~~不出意外,翻車了。。。

最后有客戶說了,你制造出來的 car 體驗不是很好,想把車再改造改造,可是車已經出廠了還能改造嗎?那這應該怎么辦呢?不要急,好說好說,我們只要能再拿到 Builder 對象就有辦法。下面我們給 Builder 添加如下構造,再對比下 Car 的構造看看有啥奇特之處:

 1       /**
2         * 回廠重造
3         * @param car
4         */

5        public Builder(Car car) {
6            this.carBody = car.carBody;
7            this.safetyBelt = car.safetyBelt;
8            this.decoration = car.decoration;
9            this.tyre = car.tyre;
10            this.aimingCircle = car.aimingCircle;
11            this.engine = car.engine;
12        }
13  /**
14     * car 的構造器 持有 Builder,將 builder 制造的組件賦值給 car 完成構建
15     *
16     * @param builder
17     */

18    public Car(Builder builder) {
19        this.carBody = builder.carBody;
20        this.tyre = builder.tyre;
21        this.engine = builder.engine;
22        this.aimingCircle = builder.aimingCircle;
23        this.decoration = builder.decoration;
24        this.safetyBelt = builder.safetyBelt;
25    }

咦,似乎有着對稱的關系,沒錯。我們提供對應的構造。調用返回對應的對象,可以實現返回的效果。在 Car 中添加方法

1 /**
2     * 重新拿回builder 去改造car
3     * @return
4     */

5    public Builder newBuilder() {
6        return new Builder(this);
7    }

現在來試試能不能返廠重建?把原來的寶馬車重造成奔馳車,調用代碼:

1Car newCar = car.newBuilder()
2                .carBody("奔馳")
3                .safetyBelt("奔馳")
4                .tyre("奔馳")
5                .aimingCircle("奔馳")
6                .decoration("奔馳")
7                .engine("奔馳")
8                .build();

行,車改造好了,我們繼續 debug ,試試改造完滿不滿意

car 改造.pngcar 改造.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 返廠重建:

 1final HttpUrl url;
2  final String method;
3  final Headers headers;
4  final RequestBody body;
5  final Object tag;
6
7  private volatile CacheControl cacheControl; // Lazily initialized.
8
9  Request(Builder builder) {
10    this.url = builder.url;
11    this.method = builder.method;
12    this.headers = builder.headers.build();
13    this.body = builder.body;
14    this.tag = builder.tag != null ? builder.tag : this;
15  }
16
17  public Builder newBuilder() {
18    return new Builder(this);
19  }

Builder 有兩個構造,第一個空構造中初始化兩個默認值。第二個構造持有 Request 用於重新構建 Builder 返廠重建。

 1public Builder() {
2      this.method = "GET";
3      this.headers = new Headers.Builder();
4    }
5
6    Builder(Request request) {
7      this.url = request.url;
8      this.method = request.method;
9      this.body = request.body;
10      this.tag = request.tag;
11      this.headers = request.headers.newBuilder();
12    }

剩下的就是一些屬性初始化的方法,返回值為 Builder 方便鏈式調用。這里就列出一個方法,詳細的請查看源碼,最后調用 build() 方法 初始化 Request 傳入 Builder 完成構建。

 1  public Builder url(HttpUrl url{
2      if (url == nullthrow new NullPointerException("url == null");
3      this.url = url;
4      return this;
5    }
6...此處省略部分方法
7  public Request build() {
8      if (url == nullthrow new IllegalStateException("url == null");
9      return new Request(this);
10    }
2、在 android 源碼中 AlertDialog 使用

在 AlertDialog 中使用到的 Builder 模式也是這種套路,我相信如果前面理解了,自己去看看源碼應該是手到擒來的事。由於篇幅原因,在這里就不展開了。

結語:個人覺得 對於設計模式的學習是相當有必要的,有時候我們需要去讀一下常用開源框架的源碼,不僅可以從中學習到一些設計思想,還可以方便日常使用。在一篇博客上面看到這句話 " 我們不重復造輪子不表示我們不需要知道輪子該怎么造及如何更好的造!",而設計模式便是讀懂框架源碼的基石,因為往往優秀的框架都會涉及很多設計模式。后面本人也會不斷更新,不斷學習新的設計模式,進而總結出來~

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

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

張少林同學.jpg張少林同學.jpg


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM