六.建造者模式:詳解構造函數、set方法、建造者模式三種對象創建方式
- Builder 模式,中文翻譯爲建造者模式或者構建者模式,也有人叫它生成器模式。
1. 爲什麼需要建造者模式?
- 在平時的開發中,創建一個對象最常用的方式是,使用 new 關鍵字調用類的構造函數來完成。我的問題是,什麼情況下這種方式就不適用了,就需要採用建造者模式來創建對象呢?
- 例子:類似線程池
因爲 maxTotal、maxIdle、minIdle 不是必填變量,所以在創建 ResourcePoolConfig 對象的時候,我們通過往構造函數中,給這幾個參數傳遞 null 值,來表示使用默認值
public class ResourcePoolConfig {
private static final int DEFAULT_MAX_TOTAL = 8;
private static final int DEFAULT_MAX_IDLE = 8;
private static final int DEFAULT_MIN_IDLE = 0;
private String name;
private int maxTotal = DEFAULT_MAX_TOTAL;
private int maxIdle = DEFAULT_MAX_IDLE;
private int minIdle = DEFAULT_MIN_IDLE;
public ResourcePoolConfig(String name, Integer maxTotal, Integer maxIdle, Integer minIdle) {
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("name should not be empty.");
}
this.name = name;
if (maxTotal != null) {
if (maxTotal <= 0) {
throw new IllegalArgumentException("maxTotal should be positive.");
}
this.maxTotal = maxTotal;
}
if (maxIdle != null) {
if (maxIdle < 0) {
throw new IllegalArgumentException("maxIdle should not be negative.");
}
this.maxIdle = maxIdle;
}
if (minIdle != null) {
if (minIdle < 0) {
throw new IllegalArgumentException("minIdle should not be negative.");
}
this.minIdle = minIdle;
}
}
//...省略getter方法...
}
- 問題二:如果可配置項逐漸增多,變成了 8 個、10 個,甚至更多,那繼續沿用現在的設計思路,構造函數的參數列表會變得很長,代碼在可讀性和易用性上都會變差。
解決方法:那就是用 set() 函數來給成員變量賦值,以替代冗長的構造函數
public class ResourcePoolConfig {
private static final int DEFAULT_MAX_TOTAL = 8;
private static final int DEFAULT_MAX_IDLE = 8;
private static final int DEFAULT_MIN_IDLE = 0;
private String name;
private int maxTotal = DEFAULT_MAX_TOTAL;
private int maxIdle = DEFAULT_MAX_IDLE;
private int minIdle = DEFAULT_MIN_IDLE;
public ResourcePoolConfig(String name) {
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("name should not be empty.");
}
this.name = name;
}
public void setMaxTotal(int maxTotal) {
if (maxTotal <= 0) {
throw new IllegalArgumentException("maxTotal should be positive.");
}
this.maxTotal = maxTotal;
}
public void setMaxIdle(int maxIdle) {
if (maxIdle < 0) {
throw new IllegalArgumentException("maxIdle should not be negative.");
}
this.maxIdle = maxIdle;
}
public void setMinIdle(int minIdle) {
if (minIdle < 0) {
throw new IllegalArgumentException("minIdle should not be negative.");
}
this.minIdle = minIdle;
}
//...省略getter方法...
}
- 使用
// ResourcePoolConfig使用舉例
ResourcePoolConfig config = new ResourcePoolConfig("dbconnectionpool");
config.setMaxTotal(16);
config.setMaxIdle(8);
- 問題
- 必填的配置項有很多,把這些必填配置項都放到構造函數中設置,那構造函數就又會出現參數列表很長的問題。如果我們把必填項也通過 set() 方法設置,那校驗這些必填項是否已經填寫的邏輯就無處安放了
- 配置項之間有一定的約束條件,比如,maxIdle 和 minIdle 要小於等於 maxTotal。如果我們繼續使用現在的設計思路,那這些配置項之間的依賴關係或者約束條件的校驗邏輯就無處安放了
- 如果我們希望 ResourcePoolConfig 類對象是不可變對象,也就是說,對象在創建好之後,就不能再修改內部的屬性值。要實現這個功能,我們就不能在 ResourcePoolConfig 類中暴露 set() 方法
- 建造者模式解決問題
public class ResourcePoolConfig {
private String name;
private int maxTotal;
private int maxIdle;
private int minIdle;
private ResourcePoolConfig(Builder builder) {
this.name = builder.name;
this.maxTotal = builder.maxTotal;
this.maxIdle = builder.maxIdle;
this.minIdle = builder.minIdle;
}
//...省略getter方法...
//我們將Builder類設計成了ResourcePoolConfig的內部類。
//我們也可以將Builder類設計成獨立的非內部類ResourcePoolConfigBuilder。
public static class Builder {
private static final int DEFAULT_MAX_TOTAL = 8;
private static final int DEFAULT_MAX_IDLE = 8;
private static final int DEFAULT_MIN_IDLE = 0;
private String name;
private int maxTotal = DEFAULT_MAX_TOTAL;
private int maxIdle = DEFAULT_MAX_IDLE;
private int minIdle = DEFAULT_MIN_IDLE;
public ResourcePoolConfig build() {
// 校驗邏輯放到這裏來做,包括必填項校驗、依賴關係校驗、約束條件校驗等
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("...");
}
if (maxIdle > maxTotal) {
throw new IllegalArgumentException("...");
}
if (minIdle > maxTotal || minIdle > maxIdle) {
throw new IllegalArgumentException("...");
}
return new ResourcePoolConfig(this);
}
public Builder setName(String name) {
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("...");
}
this.name = name;
return this;
}
public Builder setMaxTotal(int maxTotal) {
if (maxTotal <= 0) {
throw new IllegalArgumentException("...");
}
this.maxTotal = maxTotal;
return this;
}
public Builder setMaxIdle(int maxIdle) {
if (maxIdle < 0) {
throw new IllegalArgumentException("...");
}
this.maxIdle = maxIdle;
return this;
}
public Builder setMinIdle(int minIdle) {
if (minIdle < 0) {
throw new IllegalArgumentException("...");
}
this.minIdle = minIdle;
return this;
}
}
}
// 這段代碼會拋出IllegalArgumentException,因爲minIdle>maxIdle
ResourcePoolConfig config = new ResourcePoolConfig.Builder()
.setName("dbconnectionpool")
.setMaxTotal(16)
.setMaxIdle(10)
.setMinIdle(12)
.build();
解釋:校驗邏輯放置到 Builder 類中,先創建建造者,並且通過 set() 方法設置建造者的變量值,然後在使用 build() 方法真正創建對象之前,做集中的校驗,校驗通過之後纔會創建對象。除此之外,我們把 ResourcePoolConfig 的構造函數改爲 private 私有權限。這樣我們就只能通過建造者來創建 ResourcePoolConfig 類對象。並且,ResourcePoolConfig 沒有提供任何 set() 方法,這樣我們創建出來的對象就是不可變對象了。
- 建造者模式:如果構造函數參數過多,我們就需要考慮使用建造者模式,先設置建造者的變量,然後再一次性地創建對象,讓對象一直處於有效狀態。
2. 與工廠模式有何區別?
- 工廠模式
工廠模式是用來創建不同但是相關類型的對象(繼承同一父類或者接口的一組子類),由給定的參數來決定創建哪種類型的對象
- 建造者模式
建造者模式是用來創建一種類型的複雜對象,通過設置不同的可選參數,“定製化”地創建不同的對象。
- 顧客走進一家餐館點餐,我們利用工廠模式,根據用戶不同的選擇,來製作不同的食物,比如披薩、漢堡、沙拉。對於披薩來說,用戶又有各種配料可以定製,比如奶酪、西紅柿、起司,我們通過建造者模式根據用戶選擇的不同配料來製作披薩
3. 使用場景,避免過度使用
- 如果一個類中有很多屬性,爲了避免構造函數的參數列表過長,影響代碼的可讀性和易用性,我們可以通過構造函數(必填參數放在構造方法中)配合 set() 方法來解決。但是,如果存在下面情況中的任意一種,我們就要考慮使用建造者模式了
- 必填參數很多,不能全放在構造方法,放在set()方法,無法校驗是否已填
- 如果類的屬性之間有一定的依賴關係或者約束條件,我們繼續使用構造函數配合 set() 方法的設計思路,那這些依賴關係或約束條件的校驗邏輯就無處安放了。
- 如果我們希望創建不可變對象,也就是說,對象在創建好之後,就不能再修改內部的屬性值,要實現這個功能,我們就不能在類中暴露 set() 方法。構造函數配合 set() 方法來設置屬性值的方式就不適用了。