設計模式之建造者(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方法去設定物件屬性。我們提供無參的建構函式,暴露一些公共的方法讓使用者自己去設定物件屬性。這種方法較之第一種似乎增強了靈活度,使用者可以根據自己的需要隨意去設定屬性。但是這種方法自身存在嚴重的缺點:
- 因為構造過程被分到了幾個呼叫中,在構造中 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;
}
}
可見,對於複雜物件的構建,上述兩種方法都存在一定的問題。建造者模式正是為解決上述問題而出現的。
建造者模式
便捷地建立物件,使用者無需關心具體建立過程
我們使用者一般不會自己來完成 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 = "賓士"
已經改造好了,客戶相當滿意~~
下面分析一下具體是怎麼構建的:
- 新建靜態內部類 Builder ,也就是汽車製造商,我們的Car 交給他來製造,Car 的屬性全部複製進來
- 定義 Builder 空構造,初始化Car 預設值。這裡是為了初始化構造的時候,不要再去特別定義屬性,直接使用預設值
- 定義 Builder 構造,傳入Car,構造裡面執行 Car 屬性賦值 給 Builder 對應屬性的操作,目的是為了重建一個builder 進行返廠重造
- Builder中定義一系列方法進行屬性初始化,這些方法跟 JavaBeans 模式構建 中的方法類似,不同的是:為了方便鏈式呼叫,返回值為 Builder 型別
- 最後在Builder中定義
build()
方法返回實體Car物件,Car的構造器持有 Builder,最終將builder製造的元件賦值給 car 完成構建
至此,我們的 Builder 模式體驗就結束了。
優點:
- 解耦,邏輯清晰。統一交由Builder類構造,Car類不用關心內部實現細節,只注重結果
- 鏈式呼叫,使用靈活,易於擴充套件。相對於方法一中的構造器方法,配置物件屬性靈活度大大提高,支援鏈式呼叫使得邏輯清晰不少,而且我們需要擴充套件的時候,也只需要新增對應擴充套件屬性即可,十分方便
缺點:
- 硬要說缺點的話 就是前期需要編寫更多的程式碼,每次構建需要先建立對應的 Builder 物件
解決方法: 不會偷懶的程式猿不是好程式猿,針對以上缺點,IDEA 系列的 ide ,有相應的外掛 InnerBuilder 可以自動生成 builder 相關程式碼,安裝自行 google,使用的時候只需要在實體類中 alt + insert 鍵,會有個 build 按鈕提供程式碼生成。
實際應用
一般如果類屬性在4個以上的話,建議使用 此模式。還有如果類屬性存在不確定性,可能以後還會新增屬性時使用,便於擴充套件。