- 參考書籍: 《Design Patterns: Elements of Reusable Object-Oriented Software》
-
- 設計模式中一句出現頻率非常高的話是,“ 在不改動。。。。的情況下, 實現。。。。的擴展“ 。
- 對於設計模式的學習者來說,充分思考這句話其實非常重要, 因爲這句往往只對框架/ 工具包的設計纔有真正的意義。因爲框架和工具包存在的意義,就是爲了讓其他的程序員予以利用, 進行功能的擴展,而這種功能的擴展必須以不需要改動框架和工具包中代碼爲前提
- 對於應用程序的編寫者, 從理論上來說, 所有的應用層級代碼至少都是處於可編輯範圍內的, 如果不細加考量, 就盲目使用較爲複雜的設計模式, 反而會得不償失, 畢竟靈活性的獲得, 也是有代價的。
創建者模式(Builder)
設計意圖
- 將一個複雜對象的構建與展現分離, 使得同樣的構建過程可以創造出不同的展現形式。
- 解析一個複雜的對象, 將其創建爲多個目標中的一個。
- 舉例: 考慮一個RTF(Rich Text Format) reader, 它可能會需要把一個RTF文檔轉換成無格式的 ASCII 文本, 也有可能會被轉換成一個可交互的文本部件(Text Widget, 例如通過嵌入腳本實現的的可交互網頁文字小部件)。 問題在於, 同一種文本有可能會被轉換成無限數量的格式, 但是reader 只有一個。 所以應該實現在不需要改動reader 代碼的情況下, 完成可轉換格式的增添。
解決方案
抽象過後的結果如下
圖例說明
- 圖片中的空心三角箭頭,代表着繼承(extends)或實現(Implement)關係, 由繼承者/實現者 指向 被繼承者/被繼承者。
- 圖片中的實心三角箭頭且箭頭末尾沒有圓圈的, 代表着單一的引用關係, 但是被引用的對象也有可能被其他對象引用。
- 圖片中的實心三角箭頭且箭頭末尾有圓圈的, 代表着一對多的引用關係。
- 圖片中的虛線實心三角箭頭, 代表着創建或者實例化的關係。
- 圖片中的末端有圓圈的虛線是一個對方法體內容用僞代碼說明的關係
- 思考:
- RTFReader 中持有一個TextConverter 對象的引用, 而TextConverter 有多種子類實現, 如果想要擴展可被轉換的文本格式, 只要增加一個TextConverter 的實現類就完成了, 這好像只是一個通過繼承來實現代碼複用的情景, 爲何會被單獨列爲一個創建者設計模式。
- 值得注意的是 ParseRTF()中的內容, 該方法的內容其實是RTF的解析算法, 在解析的過稱中, 負責文本轉換功能的TextConverter 被不斷調用 。 在解析完成之後, TextConverter 也完成了整個文檔的轉換工作。
- 在parseRTF被執行完畢之後, 此時則可以調用TextConverter子類的GetXXXText() 方法.
- 注意: *TextConverter中並沒有一個GetText 方法。 這說明RTFReader 中是無法調用GetXXXText()方法的。*
- 原因在於, 不同的Builder所產生的產品差別過大, 沒有辦法爲不同的產品定義一個共同的父類和接口。 具體到圖中的例子, 或許ASCIIText 和TeXText可以共同繼承一個抽象父類或共同實現一個接口, 但是ASCIIText 和 TextWidet 就明顯差別過大, 不可能有一個共同的接口或父類。
- 實際上, 這些產品也沒有必要擁有一個共同的父類, 以期TextConverter中能夠定義一個公共的返回值。原因在於客戶端代碼(這裏的客戶端是指使用RTFReader的代碼)會負責將TextConverter實例化爲特定類別的轉換器對象。 最終的GetXXXX操作也是客戶端完成調用的。 具體過程如下
- 由上圖可以看出, client 代碼是負責實例化ASCIITextConverter對象的, 因此client 持有的引用並不是TextConverter類型, 所以也可以直接調用GetASCIIText獲取返回結果。
- ASCIITextConverter 中僅僅實現了convertCharacter()方法, 而parseRTF中同樣還會調用 convertParagraph(), convertFontChange() 方法。 ASCIITextConverter會直接忽略這兩種轉換請求, 什麼操作也不進行。
- RTFReader 中持有一個TextConverter 對象的引用, 而TextConverter 有多種子類實現, 如果想要擴展可被轉換的文本格式, 只要增加一個TextConverter 的實現類就完成了, 這好像只是一個通過繼承來實現代碼複用的情景, 爲何會被單獨列爲一個創建者設計模式。
創建者模式中的Builder
- 創建者模式中, builder是一個共用的接口或者父類, 其中包含了buildPart1, buildPart2, buildPart3 的方法。 這就強制了最終生成的產品, 其整體結構是完全一致的(即使不一致,也只能是一個子集關係)
創建者模式中的Director
- 創建者模式中, Director 也是一個共用的類, Director中的construct 方法決定了buildPartX() 的執行次序和執行邏輯。 同樣,這個邏輯必須足夠泛化, 對於潛在的無限產品類型是通用的。
創建者模式與工廠類模式(抽象工廠、 工廠方法)的區別
和工廠方法模式裏對象被一氣呵成創建的方式(直接調用create方法) 不同, 創建者模式構建對象的過程是在Director的控制下一步一步進行的, 有了director 的參與, 可以保證產品只有在必要的構建步驟全部執行以後, 才能通過GetProduct 方法取得。 因此builder 的接口,比其他的創建型模式都更能反應產品的構建過程(分幾個步驟構建, 有哪些大的部件), 這樣就可以實現對產品創建過程更加細粒度的控制。
抽象工廠模式與創建者模式在結構上看起來會很相似, Builder 中的接口定義了buildPart1, buildPart2, buildPart3。 AbstractFactory 中定義了createProductA, createProductB, createProductC。 然後都由不同的子類去重寫這些方法。 但他們的實際區別是:
- 抽象工廠方法關注的是產品系列的創造, 目的在於保證不同系列的產品之間不會被混用。 同時, 每個系列都產生的產品類型都是完整而全面的。
- 創建者模式關注的是是一個複雜產品的分步驟創造, 目的在於在不改動產品的構建過程(分幾個步驟構建, 有哪些部件)的情況下, 實現創建的產品類型的擴展。
- 抽象工廠中最終輸出的是一個系列的多個產品。
- 創建者模式最終輸出的是一個由多個零部件組裝成的複雜的單個產品
創建者模式分析
- 回顧創建者模式的設計動機: 將一個複雜對象的構建與展現分離, 使得同樣的構建過程可以創造出不同的展現形式
- 其實這句話對應到GOF書中的例子上來, 總覺得有點不合適。 創建者模式與其說分離了構建與展現, 倒不如說分離了創建流程與零部件的製造 ,可以在不改變創建流程的情況下, 完成新的一套零部件的擴展。
- 值得思考的是, 創建者模式與工廠方法模式相比, 真正的好處是什麼?
- GOF書中提到的是對創建流程可以實現更細粒度的控制, 但實際上工廠方法中的創建流程也是可以隨意控制的,而且相比創建者模式, 更加靈活, 畢竟不同的產品之間不需要共享同一個創建流程, 也不需要把不同產品, 都劃分出統一的部件。
- 所以歸根結底, 創建者模式的使用至少需要滿足以下3個條件:
- 一個產品的構建流程比較複雜,但是該流程對於同一個產品的不同類型來說是可以通用的 ,因此會希望這個構建流程可以被複用
- 例如將一個RTF 文檔構建成 TeX文本或ASCIIText 的文本都是依次對RTF每一個標識符依次進行識別, 完成文本邏輯層面【單個文字, 段落, 字體】的相應轉換)。 且RTF的解析流程是比較複雜的, 這個流程我們希望可以複用。
- 一個產品可以被劃分爲多個部件, 這些部件的粒度, 對於不同類型的產品也是通用的
- 不同類型的文檔都可以被劃分爲單個文字,段落的部分)。
- 一個產品存在被不斷擴展類型的“剛性需求“, 且在這個過程中, 我們不希望修改創建的流程。
- 這個要求最爲微妙, 也最容易被人們忽略,造成模式的誤用。 注意到RTFReader的例子中, 我們不想修改RTFReader , 而完成對可轉換文本類型的擴展。 其原因除了想複用Reader 的流程以外, 更爲重要的一個原因是, RTFReader 這種代碼, 往往是作爲工具包類庫,或者框架的一部分發布給程序員使用的, 所以工具包需要支持在不被改動的情況下, 讓程序員通過繼承或實現接口的方式來完成定製化格式文本的擴展。
- 一個產品的構建流程比較複雜,但是該流程對於同一個產品的不同類型來說是可以通用的 ,因此會希望這個構建流程可以被複用