最近重讀GOF的《設計模式》,讀到Builder模式的時候,發現還是不能領悟;網上搜了下其他人的解釋,發現很多人都用錯了Builder模式,結構形似Builder,實際上卻更像Template、或者Factory Method,或者四不像,并沒有體現出Builder模式的思想和威力;通過對比學習,也逐漸加深了我對Builder模式的認識,于是就有了這篇文章。
0. GOF - Builder模式
下面是GOF對Builder模式的部分闡述,先列出來,用于與后文中的錯誤案例進行對比。文字很精辟,不易理解;但若真正理解了,會發現這些文字對已經將Builder模式的精髓描述完了。
(1) 意圖:將一個復雜對象的構建與它的表示分離,使得同樣的構建過程可以創建不同的表示。
(2) 適用性:
當同時滿足以下情況的時候可以使用Builder模式
a. 當創建復雜對象的算法應該獨立于該對象的組成部分以及他們的裝配方式;
b. 當構造過程必須允許構造的對象有不同的表示;
(3) 結構:
留意圖中紅色注釋部分,尤其是循環,灰常灰常重要,呵呵,這也是很多人在使用Builder模式時所忽略的部分。(想想,為什么不是一句builder->BuildPart()就夠了,為什么要有這個循環呢?)
(5) 相關模式:
Composite通常是用Builder生成的。
先不解釋這些文字,先看兩個例子,看看使用Builder模式的誤區。這兩個例子皆來自書上(對不起,我不是惡意挑刺,實在是兩位大哥太出名了-_-,還請見諒)。
1. 組合不同數量的現有部件,需要定義新的Builder子類嗎?
先看一個例子:Builder模式應用實踐

(此圖從原文中copy來的)
這個例子中,設備(Equipment)是一個復雜對象,由一個Machine和一個(或多個)輸入端口(InputPort)或者輸出端口(OutputPort)組成;此設計中定義了一個LCDFactory(充當導向器[Director]的角色)、一個設備生成器(EQPBuilder),及三個ConcreteBuilder:
InputEQPBuilder生成的Equipment = 1個Machine + 1個InputPort;
OutputEQPBuilder生成的Equipment = 1個Machine + 1個OutputPort;
IOPutEQPBuilder生成的Equipment = 1個Machine + 1 個InputPort + 1個OutputPort;
此設計對復雜對象Equipment的創建過程進行了封裝,在應對需求變化上,作者的解釋是:“例如要求創建的Equipment包含一個Machine對象,一個Input類型的Port,兩個Output類型的Port,那么我們可以在不修改原有程序集的前提下,新定義一個IO2PutEQPBuilder類,并繼承自抽象類EQPBuilder”。
也就是說,每當要給設備增加端口的時候,我們就要創建新的Builder子類。我們把這個需求擴大化,如果要創建一個Equipment,其可能包含0~M(M>=0)個InputPort、0~N(N>=0)個OutputPort,這樣可能組合出(M+1) * (N + 1)個Equipment,因此我們就需要創建(M + 1) * (N + 1) 個ConcreteBuilder,這會帶來Builder子類數量的急劇膨脹;其本質上是通過繼承來達到構建不同的Equipment。這與Builder模式的思想是相違背的,結合Builder模式的結構圖來看,導向器(Diretor)是調用BuildPart()方法,來將部件(Part)組合到目標Product中的;如果只是組合不同數量的現有部件,則不用定義新的ConcreteBuilder。
因此,雖然這個類圖幾乎形似Builder模式,但卻并不是Builder模式的應用。
2. Builder模式就是創建復雜對象的模板嗎?
一些Builder模式的“應用”,感覺更像是一個創建復雜對象的模板;而對Builder模式與Template Method模式的區分,則認為Builder模式是側重于創建復雜對象,而Template Mehod則側重于對象的行為。
在我看來,這個觀點是錯誤的。如果把Builder當作一個創建復雜對象的模板,則基本上可以斷定,Builder模式被誤用了。Builder模式的類圖結構中,裝配復雜對象的組成部分,是用BuilderPart()方法來定義,如果我們把這個裝配操作視為一個操作行為,是不是意味著這種情況下的Builder模式就是一個Template Method了呢?我認為答案是否定的。Builder模式與Template Method模式有著天壤之別,二者毫不相干;前者偏重于通過聚合來組裝對象,后者偏重于通過繼承來重寫對象的行為。
下面再來看下,《大話設計模式》中的例子:該應用中,要求畫一個小人,要有頭、身體 、兩手、兩腳。給出的類圖結構如下所示:
(此圖從該書中copy來的)
在這個例子中,原作者要求小人不能缺胳膊少腿;我猜測,其中也隱含著小人不能有三頭六臂。作者認為“這里構造小人的‘過程’是穩定的,都需要頭身手腳,而具體構造的‘細節’是不同的,有胖有瘦有高有矮”。在應對高矮胖瘦的需求變化時,只需要增加新的PersonBuilder子類就行了。如果需要細化‘細節’,“比如人的五官、手的上臂、前臂和手掌,大腿小腿”,“這些細節是每個具體的小人都需要構建的”,則需要將這些接口加到Builder接口中;Builder模式中的Builder接口必須要“足夠普遍,以便各種類型的具體建造者構造”。
雖然這個類圖幾乎神似Builder模式,但細細斟酌,卻也有不妥的地方:
(1) 需求變化時,人可高可矮可胖可瘦,所以可以該設計中,就可為高矮胖瘦分別創建ConcreteBuilder,但假如需求繼續變化,要實現高胖、矮胖、高瘦、矮瘦呢,是否需要繼續擴展Builder?在我的理解中,高矮胖瘦體現的是“人的特征”;游戲中小人的特征可能非常多(方腦殼、圓腦殼、O型腿、八字腳、咸豬手),為創建每個不同特征的小人,都配置一個Builder,可能不太現實。
(2) 此示例中,原作者認為“構造小人的‘過程’是穩定的”;偏重于去說明,面對“細節”(即組成人的部件:頭、身、手、腳)的差異,需要創建新的ConcreteBuilder;這就容易給人造成一種錯覺:造小人的過程是一個類似于一個模板,構造不同的小人時,需要繼承該模板、重寫差異來實現新小人的生成。
(3) Builder模式構造出來的不同種類的Product,這些Product的組成部分(part)相互不能進行替換或組合,否者將會帶來ConcreteBuilder數量的急劇膨脹。這一點在后面再來說。
Builder模式的核心是“聚合”,這個例子中,并沒有把Builder模式的思想體現出來。
3. Builder模式示例
為了避免構造新的示例,便于比較和理解,我直接在上面兩個例子的基礎上進行修改:
3.1 改進版的EQPBuilder
(1). InputPort、OutputPort、Machine等,是復雜對象Equipment的組成部分,這些部件的裝配方式在AddXXOOPort、BuildMachine等方法中定義;而如何根據這些部件來創建復雜Equiment的算法,在導向器類LCDFactory中定義;這就使得“創建復雜對象的算法獨立于該對象的組成部分以及他們的裝配方式”。
(2). AddXXOOPort、BuildXXOOMachine等接口,封裝了部件(Port、machine)與產品(Equipment)的裝配方式,Add操作可能比較復雜,其可能封裝了初始化Port設備、執行插拔、焊接等操作;LCDFactory作為創建設備導向器,如果其構造設備的過程中,要增加多個Port,則只需要多次復用Builder的Add操作即可。因此,如果只是組合不同數量的現有部件,本質上只是“創建復雜對象的算法”被改變了;因為我們只用調整算法部分LCDFactory就可以了,而不用去創建新的ConcreteBuilder;
(3). 當且僅當新的部件(SuperXXOO)需要加入到系統時,才需要去創建新的ConcreteBuilder;如果要創建SuperEquipment,我們只需要將SuperEQPBuilder的示例傳遞給LCDFactory就足夠了,這里復用了原有的構造Equipment的算法。
3.2 改進版的PersonBuilder
與之前PersonBuilder的差別:
(1). 奧特曼是機器人,變形金剛是汽車人,統統可以抽象出來當人,有頭有身體有胳膊有腿(暫時不考慮汽車人變形的情況)。BuildPart(Head/Body/Arm/Leg)封裝了創建部件、并裝配人身上的操作;這個Build操作供導向器復用。PersonDirector定義了創建人的多種算法,不同算法調用BuildPart()的順序和次數不同,可以生成出具有1頭1身2臂2腿的常規人,也可以創建獨臂刀客,還可以創建三頭六臂的超人等。
(2). 構造小人的算法是靈活多變的,該算法在PersonDirector中定義;至于如何變,可以用其他設計模式來實現,這里不與討論,這里側重的是Builder模式的應用。只要我們將不同的ConcreteBuilder傳遞給同一PersonDirector,就可以得到不同的人(人類、機器人、汽車人),從而復用了創建Person的算法,達到同樣的構件過程可以創建不同的表示。
3.3 特例:StringBuilder
在這個Builder模式的實現中,Client同時充當了Director的角色;StringBuilder同時充當了Builder接口和ConcreteBuilder。這是一個最簡化的Builder模式的實現。
1: //Client同時充當了Director的角色
2: StringBuilder builder = new StringBuilder();
3: builder.Append("happyhippy");
4: builder.Append(".cnblogs");
5: builder.Append(".com");
6: //返回string對象:happyhippy.cnblogs.com
7: builder.ToString();
4. Builder模式的核心思想
將一個“復雜對象的構建算法”與它的“部件及組裝方式”分離,使得構件算法和組裝方式可以獨立應對變化;復用同樣的構建算法可以創建不同的表示,不同的構建過程可以復用相同的部件組裝方式。
抽象的Builder類,為導向者可能要求創建的每一個構件(Part)定義一個操作(接口)。這些操作缺省情況下什么都不做。一個ConcreteBuilder類對它所感興趣的構建重定義這些操作。每個ConcreteBuilder包含了創建和裝配一個特定產品的所有代碼(注意:ConcreteBuilder只是提供了使用部件裝配產品的操作接口,但不提供具體的裝配算法,裝配算法在導向器[Director]中定義)。這些代碼只需要寫一次;然后不同的Director可以復用它,以在相同部件集合的基礎上構建不同的Product。
回過頭再來看,類圖結構中對Director的注釋,為什么不是一句builder->BuildPart()就夠了,為什么要有這個循環呢?BuildPart方法封裝了創建Part、并組裝到Product中的操作,循環調用調用多次時,可以反復復用BuildPart操作,讓目標Product聚合多個Part。再進一步:如果Part中可以聚合多個Part,然后遞歸下去,可以組合成一顆樹型結構,這就是Composite了;在來理解相關模式中的這句話:“Composite通常是用Builder生成的”,就很容易理解了。
另外,需要指出的一點。單純的Builder模式中,“不同Product類型”的組成部件之間,不能進行組合或替換。譬如上面的兩個示例中:組成普通Equipment的普通InputPort、OutputPort、Machine,不允許與組成SuperEquipment的SuperInputPort、SuperOutputPort、SuperMachine進行組合創建新的Equipment;人的頭身臂腿,與奧特曼的頭身臂腿,或者汽車人的頭身臂腿,三者之間的部件不能兼容或替換。這一點GOF在DP中并沒有說明,但是在他們給出的兩個例子中,充分體現了這一點:RTF的三個轉換器,ASCIIConvert只負責組合ASCIICharactar,TeXConverter之負責組合自身格式的部件(Charactor、FontChange、Paragraph),TextWidgetConverter同理;因此不可能出現由TextWidget格式的Charactor和TeX格式的Paragraph組合而成的Text。GOF的另一個Builder模式的應用示例是StandardMazeBuilder與CountingMazeBuilder;GOF在介紹創建型模式時,前后多次用到Wall/BombedWall、Room/RoomWithABomb,為什么這里GOF偏偏不用BombedMazeBuilder,而別出心裁搞出個CountingMazeBuilder;他們很巧妙地回避了部件替換問題。假如允許“不同Product類型”的組成部件之間進行組合或替換,譬如我們允許將奧特曼的頭與變形金剛的頭進行互換,或者允許將機器人的身體替換的人的身體來構建出鋼鐵俠,或者使用其他組合來構建金剛狼,我們該怎么辦呢?這個問題已經超出了Builder模式的范疇,先留著。
相關模式[DP]:Abstract Factory與Builder相似,因為它可以創建復雜對象。主要的區別是Builder模式著重于一步步構造一個復雜對象,而Abstract Factory著重于多個系列的產品對象(簡單的或復雜的)。Builder是最后一步返回產品,而Abstract Factory是立即返回產品。Composite通常是用Builder生成的。
浙公網安備 33010602011771號