設計模式 –建類神器之建造者模式
前言
我們都知道設計模式分為創建型,結構型和行為型。創建型有,單例模式,工廠模式,建造者模式和原型模式。
今天,我們再來學習另外一個比較常用的創建型設計模式,Builder 模式,中文翻譯為建造者模式或者構建者模式,也有人叫它生成器模式。
很多博客總結的關於建造者模式的作用是:創建複雜對象的時候,用建造者模式可以使客戶端不必知道產品內部組成的細節。 這雖然是一個很重要的特徵,但是還有一個特徵以及作用很多人都並不知道。我們接下來就來好好的探討一番。
建造者模式的作用
在平時的開發中,創建一個對象最常用的方式是,使用 new 關鍵字調用類的構造函數來完成。雖然這是最簡單最常用的,但是我們仔細想一想真的是所有場景都適用嗎?
場景分析
有如下的一個場景:
有一個工廠類,這個工廠類有如下幾個成員變量:
1. 工廠名字
2. 工廠員工列表
3. 工廠設備列表
要想讓這個工廠正常的運行,這3個成員變量必須被正確賦值。那麼此時,我們最常見的方法就是在構造方法中實現這些成員變量的賦值。
當成員變量不多的時候,像上訴說的3個,這樣並沒有什麼問題,但是當成員變量變成6個,12個甚至更多,那繼續沿用現在的設計思路,構造函數的參數列表會變得很長,代碼在可讀性和易用性上都會變差。在使用構造函數的時候,我們就容易搞錯各參數的順序,傳遞進錯誤的參數值,導致非常隱蔽的 bug。
解決辦法
解決這個問題的辦法你應該也已經想到了,那就是用 set() 函數來給成員變量賦值,以替代冗長的構造函數。我們將必填的成員變量,放在我們的構造方法中,強制創建類對象的時候就要填寫。將其他不是必填的成員變量我們通過 set() 函數來設置,讓使用者自主選擇填寫或者不填寫。
這樣代碼在可讀性和易用性上提高了很多。
引入構造模式
當我們的如下幾個需求的時候,上訴使用set()函數的設計思路可能就不太滿足了
- 我們剛剛說的,將必填的成員變量放到構造方法。如果必填項很多,那構造方法又會出現我們之前說的那個問題。但是,如果我們把必填項也通過 set() 方法設置,那校驗這些必填項是否已經填寫的邏輯就無處安放了。
- 除此之外,假設配置項之間有一定的依賴關係,比如,如果用戶設置了員工列表, 就必須顯式地設置員工工資;或者配置項之間有一定的約束條件,比如,員工和工資必須一一對應。如果我們繼續使用現在的設計思路,那這些配置項之間的依賴關係或者約束條件的校驗邏輯就無處安放了。
- 如果我們希望這個工廠類對象某些屬性如name是不可變的,也就是說,對象在創建好之後,就不能再修改這些內部的屬性值。要實現這個功能,我們就不能在外部暴露 set() 方法。
為了解決這些問題,建造者模式就派上用場了。
使用建造者來實現我們的需求
我們可以把校驗邏輯放置到 Builder 類中,先創建建造者,並且通過 set() 方法設置建造者的變量值,然後在使用 build() 方法真正創建對象之前,做集中的校驗,校驗通過之後才會創建對象。除此之外,我們把 Factory 的構造函數改為 private 私有權限。這樣我們就只能通過建造者來創建 Factory 類對象。並且,Factory 沒有為不可變屬性提供任何 set() 方法,這樣我們創建出來的對象就做到了相對不可。代碼如下:
public class Factory {
private String factoryName; //工廠名字
private List<Integer> employeeIds; //員工列表
private Map<Integer,Integer> salaryMap; //員工工資
private List<String> equipmentName; //設備列表
//私有構造方法
private Factory(){}
private Factory(Builder builder) {
this.factoryName = builder.factoryName;
this.employeeIds = builder.employeeIds;
this.salaryMap = builder.salaryMap;
this.equipmentName = builder.equipmentName;
}
//...省略getter方法...
//我們將Builder類設計成了Factory的內部類。
//我們也可以將Builder類設計成獨立的非內部類FactoryBuilder。
public static class Builder {
private String factoryName;
private List<Integer> employeeIds;
private Map<Integer,Integer> salaryMap;
private List<String> equipmentName;
public Factory build() {
// 校驗邏輯放到這裡來做,包括必填項校驗、依賴關係校驗、約束條件校驗等
if (StringUtils.isBlank(factoryName)) {
throw new IllegalArgumentException("...");
}
for(Integer employeeId : employeeIds){
if (salaryMap.get(employeeId) == null) {
throw new IllegalArgumentException("...");
}
}
return new ResourcePoolConfig(this);
}
public Builder setFactoryName(String factoryName) {
if (StringUtils.isBlank(factoryName)) {
throw new IllegalArgumentException("...");
}
this.factoryName = factoryName;
return this;
}
public Builder setEmployeeIds(List<Integer> employeeIds) {
// 員工不能為0
if (employeeIds.size() == 0) {
throw new IllegalArgumentException("...");
}
this.employeeIds = employeeIds;
return this;
}
public Builder setSalaryMap(Map<Integer,Integer> salaryMap) {
if (salaryMap.size() == 0) {
throw new IllegalArgumentException("...");
}
this.salaryMap = salaryMap;
return this;
}
public Builder setEquipmentName(List<String> equipmentName) {
// 設備必須大於兩個
if (equipmentName.siez() < 2) {
throw new IllegalArgumentException("...");
}
this.minIdle = minIdle;
return this;
}
}
}
// 這段代碼會拋出IllegalArgumentException,因為設備只有一台
List<Integer> employeeIds = Collections.singletonList(1);
Map<Integer, Integer> salaryMap = Collections.singletonMap(1, 9000);
List<String> equipmentName = Collections.singletonList("新型設備");
Factory factory = new Factory.Builder()
.setFactoryName("萬能工廠")
.setEmployeeIds(employeeIds)
.setSalaryMap(salaryMap)
.setEquipmentName(equipmentName)
.build();
總結
我們來與工廠模式做一個對比,建造者模式是讓建造者類來負責對象的創建工作。工廠模式,是由工廠類來負責對象創建的工作。
那它們之間有什麼區別呢?實際上,工廠模式是用來創建不同但是相關類型的對象(繼承同一父類或者接口的一組子類),由給定的參數來決定創建哪種類型的對象。
建造者模式是用來創建一種類型的複雜對象,通過設置不同的可選參數,「定製化」地創建不同的對象。網上有一個經典的例子很好地解釋了兩者的區別。
顧客走進一家餐館點餐,我們利用工廠模式,根據用戶不同的選擇,來製作不同的食物,比如披薩、漢堡、沙拉。對於披薩來說,用戶又有各種配料可以定製,比如奶酪、西紅柿、起司,我們通過建造者模式根據用戶選擇的不同配料來製作披薩。
實際上,我們也不要太學院派,非得把工廠模式、建造者模式分得那麼清楚,我們需要知道的是,每個模式為什麼這麼設計,能解決什麼問題。只有了解了這些最本質的東西,我們才能不生搬硬套,才能靈活應用,甚至可以混用各種模式創造出新的模式,來解決特定場景的問題。