之前說過了單例模式,這周想說說建造者模式,它是另外一個比較常用的創建型設計模式。
維基百科解釋是:建造者模式,又名生成器模式,是一種對象構建模式。它可以將複雜對象的建造過程抽象出來(抽象類別),使這個抽象過程的不同實現方法可以構造出不同表現(屬性)的對象。
每種設計模式的出現,都是爲了解決一些編程不夠優雅的問題,建造者模式也是這樣。
先上一個簡單的例子
借用並改造下《Effective Java》中給出一個例子:每種食品包裝上都會有一個營養成分表,每份的含量、每罐的含量、每份卡路里、脂肪、碳水化合物、鈉等,還可能會有其他N種可選數據,大多數產品的某幾個成分都有值,該如何定義營養成分這個類呢?
無參構造器
這種方式就是標準的JavaBean方式:
public class Nutrition {
private int servingSize = -1;// required
private int servings = -1;// required
private int calories;// optional
private int fat;// optional
private int sodium;// optional
private int carbohydrate;// optional
public Nutrition() {
}
// getter and setter
}
營養成分列表實例構造過程中,需要多次調用set方法,也就是將構造實例的過程拆分成多步。這種方式有一個明顯的缺點,沒有辦法校驗必要的參數和關聯參數的有效性,比如servingSize是必要參數,如果沒有set值,只有在使用的時候才能校驗。再比如calories和fat有關聯性,兩個屬性必須同時有值或同時爲0,就需要額外在定義校驗方法。
這兩點限制都只能在客戶端進行校驗:
- 隱式校驗,在
Nutrition
類中定義校驗方法,由客戶端調用校驗方法 - 顯示校驗,由客戶端校驗對象屬性的合法性。
無論哪種校驗方法,都需要客戶端參與才行,如果客戶端忘記了,那數據有效性就沒有辦法保證了。
重疊構造器
這種方式也是比較常用的一種方式,既然無參構造器不能滿足需求,那就定義多個有參構造器:第一個構造器只有必傳參數,第二個構造器在第一個基礎上加一個可選參數,第三個加兩個,以此類推,直到最後一個包含所有參數。
代碼如下:
public class Nutrition {
private int servingSize;// required
private int servings;// required
private int calories;// optional
private int fat;// optional
private int sodium;// optional
private int carbohydrate;// optional
public Nutrition(final int servingSize, final int servings) {
this(servingSize, servings, 0, 0, 0, 0);
}
public Nutrition(final int servingSize, final int servings, final int calories) {
this(servingSize, servings, calories, 0, 0, 0);
}
public Nutrition(final int servingSize, final int servings, final int calories, final int fat) {
this(servingSize, servings, calories, fat, 0, 0);
}
public Nutrition(final int servingSize, final int servings, final int calories, final int fat, final int sodium) {
this(servingSize, servings, calories, fat, sodium, 0);
}
public Nutrition(final int servingSize, final int servings, final int calories, final int fat, final int sodium, final int carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}
// getter
}
這種寫法還可以有效解決參數校驗,只要在構造器中加入參數校驗就可以了。
如果想要初始化實例,只需要new一下就行:new Nutrition(100, 50, 0, 35, 0, 10)
。只是calories
和sodium
值爲0的時候,也需要在構造函數中明確定義是0,參數不多的情況下,也能接受。但是如果參數比較多,假設20個參數,可選參數中只有最後一個值不是0,寫起來是不是就噁心了。
還有一個隱藏缺點,那就是如果同類型參數比較多,比如上面這個例子,都是int
類型,除非每次創建實例的時候仔細對比方法簽名,否則很容易傳錯參數,這種錯誤編輯器檢查不出來,只有在運行時才能發現各種詭異錯誤。
這種方式解決了第一種方式的缺點,但是也引入了新的問題,所以需要一種更好的替代方案。
建造者模式
直接上代碼
public class Nutrition {
private int servingSize;// required
private int servings;// required
private int calories;// optional
private int fat;// optional
private int sodium;// optional
private int carbohydrate;// optional
public static class Builder {
private final int servingSize;// required
private final int servings;// required
private int calories;// optional
private int fat;// optional
private int sodium;// optional
private int carbohydrate;// optional
public Builder(final int servingSize, final int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder setCalories(final int calories) {
this.calories = calories;
return this;
}
public Builder setFat(final int fat) {
this.fat = fat;
return this;
}
public Builder setSodium(final int sodium) {
this.sodium = sodium;
return this;
}
public Builder setCarbohydrate(final int carbohydrate) {
this.carbohydrate = carbohydrate;
return this;
}
public Nutrition build() {
return new Nutrition(this);
}
}
private Nutrition(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
// getter
}
想要創建對象,只要調用new Nutrition.Builder(100, 50).setFat(35).setCarbohydrate(10).build()
就可以了。這種方式兼具前兩種方式的優點:
- 能夠毫無歧義且明確set指定屬性的值;
- 在
build
方法或Nutrition
構造函數中定義校驗方法,將校驗放在對象中;
建造者模式的缺點就是代碼變多了,這個缺點可以藉助lombok
來解決,通過註解@Builder
,可以在編譯過程自動生成對象的Builder類,相當省事。
再來一個例子
上面的例子已經覆蓋大多數情況了,下面再來一種情況。例子來源自《大話設計模式》:現在需要畫個小人,需要頭、身體、左手、右手、左腳、右腳,但是需要畫出高矮胖瘦各種不同的小人,。
通過建造者模式也可以實現這個需求,代碼如下:
public class PersonBuilder {
@Override
public void buildHead() {
System.out.println("頭");
}
@Override
public void buildBody() {
System.out.println("身體");
}
@Override
public void buildLeftHand() {
System.out.println("左手");
}
@Override
public void buildRightHand() {
System.out.println("右手");
}
@Override
public void buildLeftLeg() {
System.out.println("左腿");
}
@Override
public void buildRightLeg() {
System.out.println("右腿");
}
}
但是,如果有個方法忘記調用了,比如畫右手的方法忘記調用了,那就成楊過大俠了。這個時候就需要在建造者類之上加一個Director
類,俗稱監工。
public class PersonDirector {
private final PersonBuilder pb;
public PersonDirector(final PersonBuilder pb) {
this.pb = pb;
}
public void createPerson() {
this.pb.buildHead();
this.pb.buildBody();
this.pb.buildLeftHand();
this.pb.buildRightHand();
this.pb.buildLeftLeg();
this.pb.buildRightLeg();
}
}
這個時候,對於客戶端來說,只需要關注Director
類就行了,就相當於在客戶端調用構造器之間,增加一個監工,一個對接人,保證客戶端能夠正確使用Builder
類。
細心的朋友可能會發現,我這裏的Director
類的構造函數增加了一個Builder
參數,這是爲了更好的擴展,比如,這個時候需要增加一個胖子Builder
類,那就只需要定義一個FatPersonBuilder
,繼承PersonBuilder
,然後只需要將新增加的類傳入Director
的構造函數即可。
這也是建造者模式的另一個優點:可以定義不同的Builder
類實現不同的構建屬性,比如上面的普通人和胖子兩個Builder
類。
最後來個總結
上面給出了建造者模式兩種使用方法,第二種方法中的Director
類相當於在客戶端之間增加一個監工,第一種方法是把監工的工作放在客戶端完成,兩種方法只是在表現形式上的存在差異。
下面來總結下建造者模式的優點:
- 將一個複雜對象的創建過程封裝起來,向客戶端隱藏產品內部表現
- 允許對象通過多個步驟來創建,並可以改變過程
- 產品的實現可以變換,因爲客戶端只能看到一個抽象的接口
建造者模式作爲一種比較實用的設計模式,應用場景主要是下面兩個:
- 當創建複雜對象的算法應該獨立於該對象的組成部分以及它們的裝配方式時
- 當構造過程必須允許被構造的對象有不同的表示時
碰到上面兩種情況,不要猶豫,果斷使用建造者模式就行。
個人主頁: https://www.howardliu.cn
個人博文: 設計模式:建造者模式
CSDN主頁: http://blog.csdn.net/liuxinghao
CSDN博文: 設計模式:建造者模式