1. 程式人生 > 其它 >設計模式之建造者(Builder)模式

設計模式之建造者(Builder)模式

本文主要介紹建造者模式的原理、使用及特性。

前言

最近在接觸OkHttp原始碼的過程中看到很多設計模式的優秀應用範例。建造者模式在其中有著廣泛的應用,其核心類如OkHttpClient、Request以及Response都使用了建造者模式,可見建造者模式在OkHttp的設計中起著非常重要的作用。
那麼,建造者模式是什麼?為什麼OkHttp在設計中如此廣泛的使用該模式,使用該模式能解決什麼問題?有什麼優點和缺點呢?
剛好最近也在設計一個廣告請求模組,藉此機會好好地理解一下建造者模式。

建造者模式解決什麼問題?

在面向物件的程式設計(Object Oriented Programming, OOP)過程中,現實世界中所有的事物都可以被看作一個物件(Object)。

無論是在現實世界中還是在軟體系統中,都存在一些複雜的物件,它們擁有多個組成部分,如汽車,它包括車輪、方向盤、傳送機等各種部件。而對於大多數使用者而言,無須知道這些部件的裝配細節,也幾乎不會使用單獨某個部件,而是使用一輛完整的汽車,可以通過建造者模式對其進行設計與描述,建造者模式可以將部件和其組裝過程分開,一步一步建立一個複雜的物件。使用者只需要指定複雜物件的型別就可以得到該物件,而無須知道其內部的具體構造細節。

在軟體開發中,也存在大量類似汽車一樣的複雜物件,它們擁有一系列成員屬性,這些成員屬性中有些是引用型別的成員物件。而且在這些複雜物件中,還可能存在一些限制條件,如某些屬性沒有賦值則複雜物件不能作為一個完整的產品使用;有些屬性的賦值必須按照某個順序,一個屬性沒有賦值之前,另一個屬性可能無法賦值等。

複雜物件相當於一輛有待建造的汽車,而物件的屬性相當於汽車的部件,建造產品的過程就相當於組合部件的過程。由於組合部件的過程很複雜,因此,這些部件的組合過程往往被“外部化”到一個稱作建造者的物件裡,建造者返還給客戶端的是一個已經建造完畢的完整產品物件,而使用者無須關心該物件所包含的屬性以及它們的組裝方式,這就是建造者模式的目標。

建立物件

通常,我們建立一個物件的方法是直接呼叫其構造方法。我們來看看一隻狗狗的定義:

public class Dog {

    private String color;   // 毛色
    private String breed;   // 品種
    private int age;        // 年齡
    private boolean isMale; // 性別

    public Dog(String color, String breed, int age, boolean isMale) {
        this.color = color;
        this.breed = breed;
        this.age = age;
        this.isMale = isMale;
    }

    // get and set methods...
}

看起來很簡單。但如果我們碰到了一個複雜的物件:

比如我們用一個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模式建立物件

簡單來說就是使用set方法去設定物件屬性。我們提供無參的建構函式,暴露一些公共的方法讓使用者自己去設定物件屬性。這種方法較之第一種似乎增強了靈活度,使用者可以根據自己的需要隨意去設定屬性。但是這種方法自身存在嚴重的缺點:

  1. 因為構造過程被分到了幾個呼叫中,在構造中 JavaBean 可能處於不一致的狀態。類無法僅僅通過判斷構造器引數的有效性來保證一致性。
  2. 如果引數之間存在依賴關係,這種方式無法保證設定引數的順序。
  3. 還有一個嚴重的弊端是,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;
    }
}

可見,對於複雜物件的構建,上述兩種方法都存在一定的問題。建造者模式正是為解決上述問題而出現的。

建造者模式

便捷地建立物件,使用者無需關心具體建立過程

我們使用者一般不會自己來完成 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 methods...

    @Override
    public String toString() {
        return "Car{" +
                "carBody='" + carBody + '\'' +
                ", tyre='" + tyre + '\'' +
                ", engine='" + engine + '\'' +
                ", aimingCircle='" + aimingCircle + '\'' +
                ", safetyBelt='" + safetyBelt + '\'' +
                ", decoration='" + decoration + '\'' +
                '}';
    }

    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();
 System.out.print(car.toString());

執行結果:

carBody = "寶馬",
tyre = "寶馬",
engine = "寶馬",
aimingCircle = "寶馬",
safetyBelt = null,
decoration = "寶馬"

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

 //配置car的車身為 賓士
 Car car = new Car.Builder()
                 .carBody("賓士")
                 .build();

執行結果:

carBody = "賓士",
tyre = "寶馬",
engine = "寶馬",
aimingCircle = "寶馬",
safetyBelt = null,
decoration = "寶馬"

咦,神奇的定製 car 定製成功了。

物件一旦建立即不可變

可以看到,上述Car類中的屬性均使用final關鍵字進行修飾,保證了Car物件一旦建立就是不可變的。在有些場景(如需要將Car物件作為HashMap的key時)中非常有用。

方便地對屬性增加一些限制

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

 public Builder safetyBelt(String safetyBelt) {
      if (safetyBelt == null) throw new NullPointerException("沒系安全帶,你開個毛車啊");
      this.safetyBelt = safetyBelt;
      return this;
 }

然後呼叫的時候:

 Car car = new Car.Builder()
                 .carBody("賓士")
                 .safetyBelt(null)
                 .build();

我們給配置安全帶屬性加了 null 判斷,一但配置了null 屬性,即會丟擲異常。

方便地對已有物件進行改造,建立新的物件

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

 /**
 * Builder的建構函式:回廠重造
 * @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();

執行結果:

carBody = "賓士",
tyre = "賓士",
engine = "賓士",
aimingCircle = "賓士",
safetyBelt = 賓士,
decoration = "賓士"

已經改造好了,客戶相當滿意~~

下面分析一下具體是怎麼構建的:

  1. 新建靜態內部類 Builder ,也就是汽車製造商,我們的Car 交給他來製造,Car 的屬性全部複製進來
  2. 定義 Builder 空構造,初始化Car 預設值。這裡是為了初始化構造的時候,不要再去特別定義屬性,直接使用預設值
  3. 定義 Builder 構造,傳入Car,構造裡面執行 Car 屬性賦值 給 Builder 對應屬性的操作,目的是為了重建一個builder 進行返廠重造
  4. Builder中定義一系列方法進行屬性初始化,這些方法跟 JavaBeans 模式構建 中的方法類似,不同的是:為了方便鏈式呼叫,返回值為 Builder 型別
  5. 最後在Builder中定義build()方法返回實體Car物件,Car的構造器持有 Builder,最終將builder製造的元件賦值給 car 完成構建

至此,我們的 Builder 模式體驗就結束了。

優點:

  • 解耦,邏輯清晰。統一交由Builder類構造,Car類不用關心內部實現細節,只注重結果
  • 鏈式呼叫,使用靈活,易於擴充套件。相對於方法一中的構造器方法,配置物件屬性靈活度大大提高,支援鏈式呼叫使得邏輯清晰不少,而且我們需要擴充套件的時候,也只需要新增對應擴充套件屬性即可,十分方便

缺點:

  • 硬要說缺點的話 就是前期需要編寫更多的程式碼,每次構建需要先建立對應的 Builder 物件
    解決方法: 不會偷懶的程式猿不是好程式猿,針對以上缺點,IDEA 系列的 ide ,有相應的外掛 InnerBuilder 可以自動生成 builder 相關程式碼,安裝自行 google,使用的時候只需要在實體類中 alt + insert 鍵,會有個 build 按鈕提供程式碼生成。

實際應用

一般如果類屬性在4個以上的話,建議使用 此模式。還有如果類屬性存在不確定性,可能以後還會新增屬性時使用,便於擴充套件。

參考