Design Pattern(10) - Builder
June 29, 2017
一個constructor參數很多的class
今天我們有一個class Employee
他有很多attribute
即使你覺得這麼長的constructor沒有問題 可是在創一個物件的時候 有些attribute是必須的 有些是不一定要有的(比如說age跟birthday跟address不一定要提供) 那怎麼辦呢?
寫過C的都知道 很簡單的default parameter搞定
輕鬆 所有optional的值我都先寫好 只需要一個constructor
但對不起 java有java的玩法
java沒有default parameter
那怎麼辦呢 不好意思 你必須overload你的constructor
題外話
上面那個C的程式不能那樣寫 所有的optional的參數應該擺在參數的最後面 不可以想擺哪就擺哪
理由是如果今天你擺中間
Compiler不知道你要assign 7給誰
回到overload
所以你只能這樣
所有的constructor都要call最長的那個constructor
還必須得照重要性排 你無法創一個只有address但沒有birthday跟age的Employee
(你不能多一個只有address沒有birthday和age的constructor 因為他跟只有birthday沒有address和age的constructor有一樣的signature)
福無雙至 禍不單行 缺點還沒說完 即使是上面的實作 還是有缺點 今天如果想改address的default value變成”Not Applicable” 你必須改所有constructor的code
要怎麼解決constructor code重複的問題呢
給你一分鐘
解答叫做 telescoping constructor
5個參數的constructor 多增加一個參數 call 6個參數的constructor
6個參數的constructor 多增加一個參數 call 7個參數的constructor
7個參數的constructor 多增加一個參數 call 8個參數的constructor
是否覺得有點神似
這樣子你某個optional的argument如果要改 就只要改一個地方
回到正題
這實在醜 而且client每次要call都要看一下到底參數順序要怎麼給 而且重要性的問題也還沒解答
你無法創一個只有address但沒有birthday跟age的Employee
黑暗盡頭 主角登場
我們在Employee裡面加一個inner class, Builder
client怎麼建一個Employee?
對client來說code非常好懂 也解決了之前的問題 我們可以只給address不用給birthday跟age
除此之外 還有什麼好處呢?
-
對於不合法的輸入及早發現及早治療: 你可以在每一個setter都做好error handling 之前的例子可能你constructor都跑一半了才發現user給你髒東西
這裡的話setter直接噴錯就可以 不用等到build()被call
-
如果你要給的某個參數是array of something 你需要用到varargs 那你的constructor就無法支援其中兩個參數是array of something的情況 不過這點其實用List也可以解決
-
如果參數的順序有強制的話 比如給參數A之前 參數B必須已經有了的情況 constructor做不到 可是builder可以輕易做到
問問題時間
Q: 之前說好分離一個物件的創造和使用 factory就是另一個class專門負責創造 為什麼builder需要是inner class?
A: 因為constructor是private 我們不想讓其他人去call這個constructor(Builder應該要是唯一可以call constructor的人) 所以Builder是static inner class
Q: 為什麼不能就讓setXXX當成Employee的member function 還特別需要一個Builder? 這樣的話我呼叫完constructor完之後再慢慢set不是也一樣效果嗎?
A: 因為可能會有一些invalid state 或是參數之間有dependency的話 這個做法就不好 另一個缺點是這樣我們無法建造一個無法被改變狀態的class 因為setXXX function都必須要是public
以上
以上是effective java中Chap2 Item2裡面對於builder的介紹 如果你是第一次看builder 那你可以不需要往下看 我認為effective java已經把使用時機跟優缺點都講得非常清楚 你已經知道怎麼用Builder跟什麼時機用Builder 我認為已經很夠了
但如果你是看了Gof以後覺得Builer跟Factory沒有什麼區別 想真的分得很清楚 那麼你可以往下看下去 因為我第一次讀builder的時候也有一樣的問題
Prerequisites
-
很不幸的 你很想了解Gof裡面的結構部分 想知道到底Director是什麼
-
你已經知道並了解什麼是工廠模式和抽象工廠
-
你有十分鐘以上的連續時間 只看到一半的話很有可能你會跟工廠模式混淆
以上條件都達成的話歡迎往下繼續看 否則走火入魔後果自行負責
先來看看GOF對於Builder的定義
建造者模式讓你可以一步步創建一個複雜對象 所以可以用同一套程序生成屬性不同的物件
結構
-
Builder(抽象建造者): 用來生成Product的抽象介面
-
ConcreteBuilder(具體建造者): 實作Builder 負責建構組裝Product的零件
-
Director: 操縱Builder介面來建構物件
麥當勞
讀Design Pattern不實作就如紙上談兵 我們照著結構圖 一個一個實作
Product就是Meal 一個麥當勞餐包含了主食 副食和飲料
記得先建好抽象的Builder
我們的ConcreteBuilder是大麥克建造者
Director是我們的Waiter
Waiter的constructor吃一個builder 你叫我build的時候我就乖乖的去拿大麥克套餐的主食 大麥克套餐的副食 大麥克套餐的飲料
Client的用法也相當直觀
很好 我們照著教科書一步步實做起來 你有興趣的話我把code放在我的github上 你可以直接在IDE上跑起來
但你有沒有發現 其實我們根本不需要這個Waiter
Waiter做的事就只是照著他的member variable: builder裡面的東西一個個把東西建出來 那這件事其實ConcreteBuilder自己就做得到
要做的修改就是把build()移到ConcreteBuilder
搞定 client用法變這樣
有沒有覺得這個好像似曾相似 你可以看仔細一點
想不起來嗎 你可以再看仔細一點
恭喜你 原來Builder根本就是抽象工廠的加工
BigMac, Fries, Coke就屬於同一個產品族
每一個不同的Concrete Builder就是不同的具體工廠(BigMacBuilder, McNuggetBuilder, CheeseburgerBuilder)
MealBuilder就是抽象工廠
原來是個用菜頭排骨酥做成的味增湯啊
融會貫通
那這兩種模式的差別又在哪裡 為什麼會這麼像呢? 再給你一圈
這兩個模式最主要的癥結是ConcreteBuilder的數量 如果ConcreteBuilder數量多於一 那根本就是抽象工廠
因為超過一個Builder代表說Product的某部分不一樣(比如說大麥克餐跟麥克雞塊餐的主食不一樣) 那就需要用到抽象工廠產品族的概念
可是這樣就失去了Builder的本意
我認為Builder就是應該要在build之前把需要的東西都設定好 然後再開始build
所以剛剛麥當勞的例子根本就可以用唯一的Builer去setFood, setDrink等等 然後call build把餐點生出來
既然ConcreteBuilder只需要一個 我們可以省略抽象的Builder 只需要一個Builder class 當然沒用的Director也可以完全不要
最後整個結構圖簡化成這樣
這就是我們文章前半段在講的東西 繞了一圈 原來之前講的就很夠了啊
難得糊塗 荒唐荒唐 醉過醉過
如果天黑之前來得及
我要忘了你的眼睛
窮極一生
做不完一場夢
大夢初醒荒唐了一生
南山南
雖然到頭來多走了一圈冤枉路
Effective Java的builder講的就已經很夠了
但回頭來看 還是不枉瀟灑走一回 痛快
原來
抽象工廠就是參數寫死的Builder
Builder就是個更有彈性的constructor
使用時機就是constructor參數很多 或是sub物件的生成順序有dependency