1. 建造者模式可以解決什麼問題
我家裏有各種形狀的瓷器,盤子或者碗。雖然形狀不同,但是所用的材料基本上是一樣的,比如土、水、釉、彩這些基本的東西。
但是做不同款式的瓷器,方法是不同的。假如說我現在已經寫好了一段代碼來生成白瓷碗和白瓷盤,正常運行起來了,這倒也沒什麼,可是,如果我現在要增加一個工作,製作彩繪馬克杯,這樣我就得修改已有的代碼了。
最簡單的修改方式就是加if-else。就像是這樣:
if (type = "白瓷碗") {
makeWhiteBowl(...);
} else if (type = "白瓷盤") {
makeWhitePlate(...);
} else if (type = "彩繪馬克杯") {
makeColorCup(...);
}
...
Well,看起來還不很嚴重,那麼如果你的業務越來越複雜呢?每次新添加一種type,首先就要去改代碼,也許還不止這一處,這是不可忍受的,是很爛的設計。
建造者模式,就是爲了解決這種問題出現的一種設計模式。
在GoF的《設計模式》一書中,明確的提及了建造者模式的意圖:
將一個複雜對象的構建與它的表示分離,使得同樣的構建過程可以創建不同的表示。
這一論斷說明了這麼幾點:
- 構建過程和對象相分離,也就是說不要直接去new這個對象;
- 構建過程抽象出來到一個高層次上。
回到我上面的例子,就是製造瓷器的人和瓷器相分離。客戶交給匠人材料和訂單,匠人自行製造交付就可以了。
2. 建造者模式的理論
建造者模式中會存在下面幾種角色:
- 建造者(Builder):接口,用於創建產品的各個部分的抽象;
- ConcreteBuilder:實現Builder接口,是具體的建造者;
- Director:這個名字看着像“導演”,其實是用於構造Builder對象的;
- Product:各類產品,最終被Builder生產出來的東西。
下圖是GoF給出的類圖:
知道了建造者模式的基本類圖結構以後,還需要知道這個模式是如何使用的,如下時序圖:
3. 具體編碼實現
在《重學Java設計模式》中的建造者模式章節提到了一個裝修的例子,但是從這個例子上來看,和GoF的描述還是有一些區別的。
還是以裝修爲例,書中提到了集中裝修風格,其實就對應了幾種ConcreteBuilder,例如田園奢華歐式、輕奢田園和現代簡約。而這些建造者們都有相同的幾種方法:
- 吊頂
- 塗牆
- 鋪地板或者地磚
另外還有物料,比如各種品牌的裝修材料等等。下面是我根據GoF的理論,對《重學Java設計模式》的建造者代碼的改造。
首先是物料,可以抽象出一個Matter接口,所有的地板,塗料都是Matter。
/**
* 物料的五種屬性
*/
public interface Matter {
//類型,比如地板
String scene();
//品牌
String brand();
//型號
String model();
//價格
BigDecimal price();
String desc();
}
物料並非建造者模式中的核心元素,接下來就可以抽象建造者了。
package com.example.pattern.builder.iface;
public interface IBuilder {
//裝吊頂
IBuilder appendCeiling(Matter ceiling);
//塗牆
IBuilder appendCoat(Matter coat);
//鋪地板
IBuilder appendFloor(Matter floor);
//鋪地磚
IBuilder appendTitle(Matter title);
//得到裝修報價單
String getResult();
}
建造者要做的工作就是來料加工,也就是一個裝修工人的手藝了。
下面是對Director的定義,可以認爲Director是監工吧,業主只和監工或者工頭直接聯繫,不會給工人直接下達任務:
package com.example.pattern.builder.builder;
import com.example.pattern.builder.iface.IBuilder;
import com.example.pattern.builder.iface.Matter;
import lombok.Data;
@Data
public class Director {
private IBuilder builder;
private Matter ceiling;
private Matter coat;
private Matter floor;
private Matter title;
public Director(IBuilder builder) {
this.builder = builder;
}
public String construct() {
return builder.appendCeiling(ceiling).appendCoat(coat)
.appendFloor(floor).appendTitle(title).getResult();
}
}
其實Director也不知道自己的物料是什麼。從時序圖上來看,不管是具體的建造者還是Director都是使用者(就像業主)提供的,因此使用者的代碼如下:
public class BuilderApp {
public static void main(String[] args) {
IBuilder europeBuilder = new EuropeBuilder(new BigDecimal(160));
Director director = new Director(europeBuilder);
director.setCeiling(new LevelTwoCeiling());
director.setFloor(new MarcoPoloTitle());
director.setCoat(new NipponCoat());
String result = director.construct();
System.out.println(result);
}
}
這個結果就是計算一下裝修的賬單,很簡單的一個示例,最終的打印是總報價。
4. 進一步的思考
在結城浩的《圖解設計模式》一書中提到了“誰知道什麼”的概念。在上面的代碼中,這個概念體現了兩次:
- BuilderApp類並沒有直接調用Builder,BuilderApp並不知道Builder能做什麼;
- Director也不知道自己到底調用的是哪個Builder,它使用的是一個接口。
OOP一定是面向接口最好的,因爲可以隨時替換掉具體的實現,不同的類之間耦合度不高。
在新增一個產品的時候,只需要添加一個具體的Builder就可以了,不需要修改現有代碼。
學習一種設計模式總是寫這種簡單的例子也感覺沒什麼意思。我在之前的工作中因爲一些原因不能用現成的第三方包,就只能寫了一個Map的生成器來做事情,我覺得這個倒是挺有建造者模式的味道的。
首先我不需要定義Map了,因爲Java.util已經提供了足夠多的,而且是接口和實現分離的。
下面需要定義一個Builder:
package com.example.pattern.builder.maphelper;
import java.util.Map;
public class MapBuilder<K, V> {
private Map<K, V> map;
public MapBuilder(Map<K, V> map) {
this.map = map;
}
public MapBuilder<K, V> put(K k, V v) {
map.put(k, v);
return this;
}
public Map<K, V> build() {
return map;
}
}
這個Builder更好的地方就是自己都不知道自己製造什麼產品。接下來時Director的角色:
package com.example.pattern.builder.maphelper;
import java.util.Map;
public class MapHelper {
public static <K, V> MapBuilder<K, V> builder(Map<K, V> map) {
return new MapBuilder<>(map);
}
}
這簡直太讓人舒適了,因爲調用者的代碼是鏈式的:
Map<String, String> map
= MapHelper.builder(new HashMap<String, String>()).put("foo", "bar")
.build();
這個建造者和GoF的理論有一點差異,就是MapHelper並沒有持有任何對象,沒有構造方法。其實如果一定要改造也是可以的。
Map本身是一個比較複雜的構建,因爲要不停的put,代碼也顯得不夠漂亮,鏈式put則顯得優雅一些。而且這個Builder本身也實現了構建和表示分離的原則。