每種設(shè)計(jì)模式的出現(xiàn),都是為了解決一些編程不夠優(yōu)雅的問題,建造者模式也是這樣。
維基百科解釋是:建造者模式 Builder Pattern,又名生成器模式,是一種對(duì)象構(gòu)建模式。它可以將復(fù)雜對(duì)象的建造過程抽象出來(抽象類別),使這個(gè)抽象過程的不同實(shí)現(xiàn)方法可以構(gòu)造出不同表現(xiàn)(屬性)的對(duì)象。
先上一個(gè)例子
借用并改造下 Effective Java 中給出的例子:每種食品包裝上都會(huì)有一個(gè)營(yíng)養(yǎng)成分表,每份的含量、每罐的含量、每份卡路里、脂肪、碳水化合物、鈉等,還可能會(huì)有其他 N 種可選數(shù)據(jù),大多數(shù)產(chǎn)品的某幾個(gè)成分都有值,該如何定義營(yíng)養(yǎng)成分這個(gè)類呢?
重疊構(gòu)造器
因?yàn)橛卸鄠€(gè)參數(shù),有必填、有選填,最先想到的就是定義多個(gè)有參構(gòu)造器:第一個(gè)構(gòu)造器只有必傳參數(shù),第二個(gè)構(gòu)造器在第一個(gè)基礎(chǔ)上加一個(gè)可選參數(shù),第三個(gè)加兩個(gè),以此類推,直到最后一個(gè)包含所有參數(shù),這種寫法稱為重疊構(gòu)造器,有點(diǎn)像疊羅漢。還有一種常見寫法是只寫一個(gè)構(gòu)造函數(shù),包含所有參數(shù)。
代碼如下:
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
}
這種寫法還可以有效解決參數(shù)校驗(yàn),只要在構(gòu)造器中加入?yún)?shù)校驗(yàn)就可以了。
如果想要初始化實(shí)例,只需要 new 一下就行:
new Nutrition(100, 50, 0, 35, 0, 10)
這種寫法,不夠優(yōu)雅的地方是,當(dāng) calories 和 sodium 值為 0 的時(shí)候,也需要在構(gòu)造函數(shù)中明確定義是 0,示例中才 6 個(gè)參數(shù),也能勉強(qiáng)接受。但是如果參數(shù)達(dá)到 20 個(gè)呢?可選參數(shù)中只有一個(gè)值不是 0 或空,寫起來很好玩了,滿屏全是 0 和 null 的混合體。
還有一個(gè)隱藏缺點(diǎn),那就是如果同類型參數(shù)比較多,比如上面這個(gè)例子,都是 int 類型,除非每次創(chuàng)建實(shí)例的時(shí)候仔細(xì)對(duì)比方法簽名,否則很容易傳錯(cuò)參數(shù),而且這種錯(cuò)誤編輯器檢查不出來,只有在運(yùn)行時(shí)會(huì)出現(xiàn)各種詭異錯(cuò)誤,排錯(cuò)的時(shí)候不知道要薅掉多少根頭發(fā)了。
想要解決上面兩個(gè)問題,不難想到,可以通過 set 方法一個(gè)個(gè)賦值就行了。
set 方式賦值
既然構(gòu)造函數(shù)中放太多參數(shù)不夠優(yōu)雅,還有缺點(diǎn),那就換種寫法,構(gòu)造函數(shù)只保留必要字段,其他參數(shù)的賦值都用 setter 方法就行了。
代碼如下:
public class Nutrition {
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 Nutrition(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
// getter and setter
}
這樣就可以解決構(gòu)造函數(shù)參數(shù)太多、容易傳錯(cuò)參數(shù)的問題,只在需要的時(shí)候 set 指定參數(shù)就行了。
如果沒有特殊需求,到這里可以解決大部分問題了。
但是需求總是多變的,總會(huì)有類似“五彩斑斕的黑”這種奇葩要求:
- 如果必填參數(shù)比較多,或者大部分參數(shù)是必填參數(shù)。這個(gè)時(shí)候這種方式又會(huì)出現(xiàn)重疊構(gòu)造器那些缺點(diǎn)。
- 如果把所有參數(shù)都用 set 方法賦值,那又沒有辦法進(jìn)行必填項(xiàng)的校驗(yàn)。
- 如果非必填參數(shù)之間有關(guān)聯(lián)關(guān)系,比如上面例子中,脂肪 fat 和碳水化合物 carbohydrate 有值的話,卡路里 calories 一定不會(huì)為 0。但是使用現(xiàn)在這種設(shè)計(jì)思路,屬性之間的依賴關(guān)系或者約束條件的校驗(yàn)邏輯就沒有地方定義了。
- 如果想要把 Nutrition 定義成不可變對(duì)象的話,就不能使用 set 方法修改屬性值。
這個(gè)時(shí)候就該祭出今天的主角了。