1. 程式人生 > >好書整理系列之-設計模式:可複用面向物件軟體的基礎 3

好書整理系列之-設計模式:可複用面向物件軟體的基礎 3


第3章建立型模式
建立型模式抽象了例項化過程。它們幫助一個系統獨立於如何建立、組合和表示它的那
些物件。一個類建立型模式使用繼承改變被例項化的類,而一個物件建立型模式將例項化委
託給另一個物件。
隨著系統演化得越來越依賴於物件複合而不是類繼承,建立型模式變得更為重要。當這
種情況發生時,重心從對一組固定行為的硬編碼( h a r d - c o d i n g)轉移為定義一個較小的基本
行為集,這些行為可以被組合成任意數目的更復雜的行為。這樣建立有特定行為的物件要求
的不僅僅是例項化一個類。
在這些模式中有兩個不斷出現的主旋律。第一,它們都將關於該系統使用哪些具體的類
的資訊封裝起來。第二,它們隱藏了這些類的例項是如何被建立和放在一起的。整個系統關
於這些物件所知道的是由抽象類所定義的介面。因此,建立型模式在什麼被建立, 誰建立它,
它是怎樣被建立的,以及何時建立這些方面給予你很大的靈活性。它們允許你用結構和功能
差別很大的“產品”物件配置一個系統。配置可以是靜態的(即在編譯時指定),也可以是動
態的(在執行時)。
有時建立型模式是相互競爭的。例如,在有些情況下P r o t o t y p e(3 . 4)或Abstract Factory
(3 . 1)用起來都很好。而在另外一些情況下它們是互補的: B u i l d e r(3 . 2)可以使用其他模式
去實現某個構件的建立。P r o t o t y p e(3 . 4)可以在它的實現中使用S i n g l e t o n(3 . 5)。
因為建立型模式緊密相關,我們將所有5個模式一起研究以突出它們的相似點和相異點。
我們也將舉一個通用的例子—為一個電腦遊戲建立一個迷宮—來說明它們的實現。這個迷
宮和遊戲將隨著各種模式不同而略有區別。有時這個遊戲將僅僅是找到一個迷宮的出口;在
這種情況下,遊戲者可能僅能見到該迷宮的區域性。有時迷宮包括一些要解決的問題和要戰勝
的危險,並且這些遊戲可能會提供已經被探索過的那部分迷宮地圖。
我們將忽略許多迷宮中的細節以及一個迷宮遊戲中有一個還是多個遊戲者。我們僅關注
迷宮是怎樣被建立的。我們將一個迷宮定義為一系列房間,一個房間知道它的鄰居;可能的
鄰居要麼是另一個房間,要麼是一堵牆、或者是到另一個房間的一扇門。
類R o o m、D o o r和Wa l l定義了我們所有的例子中使用到的構件。我們僅定義這些類中對創
建一個迷宮起重要作用的一些部分。我們將忽略遊戲者、顯示操作和在迷宮中四處移動操作,
以及其他一些重要的卻與建立迷宮無關的功能。
下頁圖表示了這些類之間的關係。
每一個房間有四面,我們使用C + +中的列舉型別D i r e c t i o n來指定房間的東南西北:
enum Direction {North, South, East, West};
S m a l l t a l k的實現使用相應的符號來表示這些方向。
類M a p S i t e是所有迷宮元件的公共抽象類。為簡化例子, M a p S i t e僅定義了一個操作E n t e r,
它的含義決定於你在進入什麼。如果你進入一個房間,那麼你的位置會發生改變。如果你試
圖進入一扇門,那麼這兩件事中就有一件會發生:如果門是開著的,你進入另一個房間。如
果門是關著的,那麼你就會碰壁。
E n t e r為更加複雜的遊戲操作提供了一個簡單基礎。例如,如果你在一個房間中說“向東
走”,遊戲只能確定直接在東邊的是哪一個M a p S i t e並對它呼叫E n t e r。特定子類的E n t e r操作將
計算出你的位置是發生改變,還是你會碰壁。在一個真正的遊戲中, E n t e r可以將移動的遊戲
者物件作為一個引數。
R o o m是M a p S i t e的一個具體的子類,而M a p S i t e定義了迷宮中構件之間的主要關係。
R o o m有指向其他M a p S i t e物件的引用,並儲存一個房間號,這個數字用來標識迷宮中的房間。
下面的類描述了一個房間的每一面所出現的牆壁或門。
第3章建立型模式5 5

我們不僅需要知道迷宮的各部分,還要定義一個用來表示房間集合的M a z e類。用
R o o m N o操作和給定的房間號, M a z e就可以找到一個特定的房間。
R o o m N o可以使用線形搜尋、h a s h表、甚至一個簡單陣列進行一次查詢。但我們在此處並
不考慮這些細節,而是將注意力集中於如何指定一個迷宮物件的構件上。
我們定義的另一個類是M a z e G a m e,由它來建立迷宮。一個簡單直接的建立迷宮的方法是
使用一系列操作將構件增加到迷宮中,然後連線它們。例如,下面的成員函式將建立一個迷
宮,這個迷宮由兩個房間和它們之間的一扇門組成:
考慮到這個函式所做的僅是建立一個有兩個房間的迷宮,它是相當複雜的。顯然有辦法使它
變得更簡單。例如, R o o m的構造器可以提前用牆壁來初始化房間的每一面。但這僅僅是將代
碼移到了其他地方。這個成員函式真正的問題不在於它的大小而在於它不靈活。它對迷宮的
佈局進行硬編碼。改變佈局意味著改變這個成員函式,或是重定義它—這意味著重新實現
整個過程—或是對它的部分進行改變—這容易產生錯誤並且不利於重用。
建立型模式顯示如何使得這個設計更靈活,但未必會更小。特別是,它們將便於修改定
5 6 設計模式:可複用面向物件軟體的基礎

義一個迷宮構件的類。
假設你想在一個包含(所有的東西)施了魔法的迷宮的新遊戲中重用一個已有的迷宮布
局。施了魔法的迷宮遊戲有新的構件,像D o o r N e e d i n g S p e l l,它是一扇僅隨著一個咒語才能被
鎖上和開啟的門;以及E n c h a n t e d R o o m,一個可以有不尋常東西的房間,比如魔法鑰匙或是
咒語。你怎樣才能較容易的改變C r e a t e M a z e以讓它用這些新型別的物件建立迷宮呢?
這種情況下,改變的最大障礙是對被例項化的類進行硬編碼。建立型模式提供了多種不
同方法從例項化它們的程式碼中除去對這些具體類的顯式引用:
• 如果C r e a t e M a z e呼叫虛擬函式而不是構造器來建立它需要的房間、牆壁和門,那麼你可以
建立一個M a z e G a m e的子類並重定義這些虛擬函式,從而改變被例化的類。這一方法是
Factory Method(3 . 3)模式的一個例子。
• 如果傳遞一個物件給C r e a t e M a z e作引數來建立房間、牆壁和門,那麼你可以傳遞不同的
引數來改變房間、牆壁和門的類。這是Abstract Factory(3 . 1)模式的一個例子。
• 如果傳遞一個物件給C r e a t e M a z e,這個物件可以在它所建造的迷宮中使用增加房間、牆
壁和門的操作,來全面建立一個新的迷宮,那麼你可以使用繼承來改變迷宮的一些部分
或該迷宮被建造的方式。這是B u i l d e r(3 . 2)模式的一個例子。
• 如果C r e a t e M a z e由多種原型的房間、牆壁和門物件引數化,它拷貝並將這些物件增加到
迷宮中,那麼你可以用不同的物件替換這些原型物件以改變迷宮的構成。這是P r o t o t y p e
(3 . 4)模式的一個例子。
剩下的建立型模式, S i n g l e t o n(3 . 5),可以保證每個遊戲中僅有一個迷宮而且所有的遊戲
物件都可以迅速訪問它—不需要求助於全域性變數或函式。S i n g l e t o n也使得迷宮易於擴充套件或替
換,且不需變動已有的程式碼。
3.1 ABSTRACT FACTORY(抽象工廠)—物件建立型模式
1. 意圖
提供一個建立一系列相關或相互依賴物件的介面,而無需指定它們具體的類。
2. 別名
K i t
3. 動機
考慮一個支援多種視感( l o o k - a n d - f e e l)標準的使用者介面工具包,例如M o t i f和
Presentation Manager。不同的視感風格為諸如滾動條、視窗和按鈕等使用者介面“視窗元件”
定義不同的外觀和行為。為保證視感風格標準間的可移植性,一個應用不應該為一個特定的
視感外觀硬編碼它的視窗元件。在整個應用中例項化特定視感風格的視窗元件類將使得以後
很難改變視感風格。
為解決這一問題我們可以定義一個抽象的Wi d g e t F a c t o r y類,這個類聲明瞭一個用來建立
每一類基本視窗元件的介面。每一類視窗元件都有一個抽象類,而具體子類則實現了視窗組
件的特定視感風格。對於每一個抽象視窗元件類, Wi d g e t F a c t o r y介面都有一個返回新視窗組
件物件的操作。客戶呼叫這些操作以獲得視窗元件例項,但客戶並不知道他們正在使用的是
哪些具體類。這樣客戶就不依賴於一般的視感風格,如下頁圖所示。
第3章建立型模式5 7

每一種視感標準都對應於一個具體的Wi d g e t F a c t o r y子類。每一子類實現那些用於建立合
適視感風格的視窗元件的操作。例如, M o t i f Wi d g e t F a c t o r y的C r e a t e S c r o l l B a r操作例項化並返
回一個M o t i f滾動條,而相應的P M Wi d g e t F a c t o r y操作返回一個Presentation Manager的滾動條。
客戶僅通過Wi d g e t F a c t o r y介面建立視窗元件,他們並不知道哪些類實現了特定視感風格的窗
口元件。換言之,客戶僅與抽象類定義的介面互動,而不使用特定的具體類的介面。
Wi d g e t F a c t o r y也增強了具體視窗元件類之間依賴關係。一個M o t i f的滾動條應該與M o t i f按
鈕、M o t i f正文編輯器一起使用,這一約束條件作為使用M o t i f Wi d g e t F a c t o r y的結果被自動加
上。
4. 適用性
在以下情況可以使用Abstract Factory模式
• 一個系統要獨立於它的產品的建立、組合和表示時。
• 一個系統要由多個產品系列中的一個來配置時。
• 當你要強調一系列相關的產品物件的設計以便進行聯合使用時。
• 當你提供一個產品類庫,而只想顯示它們的介面而不是實現時。
5. 結構
此模式的結構如下圖所示。
6. 參與者
• A b s t r a c t F a c t o r y ( Wi d g e t F a c t o r y )
5 8 設計模式:可複用面向物件軟體的基礎

— 宣告一個建立抽象產品物件的操作介面。
• C o n c r e t e F a c t o r y ( M o t i f Wi d g e t F a c t o r y,P M Wi d g e t F a c t o r y )
— 實現建立具體產品物件的操作。
• A b s t r a c t P r o d u c t ( Wi n d o w s,S c r o l l B a r )
— 為一類產品物件宣告一個介面。
• C o n c r e t e P r o d u c t ( M o t i f Wi n d o w,M o t i f S c r o l l B a r )
— 定義一個將被相應的具體工廠建立的產品物件。
— 實現A b s t r a c t P r o d u c t介面。
• C l i e n t
— 僅使用由A b s t r a c t F a c t o r y和A b s t r a c t P r o d u c t類宣告的介面。
7. 協作
• 通常在執行時刻建立一個C o n c r e t e F a c t r o y類的例項。這一具體的工廠建立具有特定實現
的產品物件。為建立不同的產品物件,客戶應使用不同的具體工廠。
• AbstractFactory將產品物件的建立延遲到它的C o n c r e t e F a c t o r y子類。
8. 效果
A b s t r a c t F a c t o r y模式有下面的一些優點和缺點:
1) 它分離了具體的類Abstract Factory模式幫助你控制一個應用建立的物件的類。因為
一個工廠封裝建立產品物件的責任和過程,它將客戶與類的實現分離。客戶通過它們的抽象
介面操縱例項。產品的類名也在具體工廠的實現中被分離;它們不出現在客戶程式碼中。
2) 它使得易於交換產品系列一個具體工廠類在一個應用中僅出現一次—即在它初始化
的時候。這使得改變一個應用的具體工廠變得很容易。它只需改變具體的工廠即可使用不同
的產品配置,這是因為一個抽象工廠建立了一個完整的產品系列,所以整個產品系列會立刻
改變。在我們的使用者介面的例子中,我們僅需轉換到相應的工廠物件並重新建立介面,就可
實現從M o t i f視窗元件轉換為Presentation Manager視窗元件。
3) 它有利於產品的一致性當一個系列中的產品物件被設計成一起工作時,一個應用一
次只能使用同一個系列中的物件,這一點很重要。而A b s t r a c t F a c t o r y很容易實現這一點。
4) 難以支援新種類的產品難以擴充套件抽象工廠以生產新種類的產品。這是因為
A b s t r a c t F a c t o r y介面確定了可以被建立的產品集合。支援新種類的產品就需要擴充套件該工廠介面,
這將涉及A b s t r a c t F a c t o r y類及其所有子類的改變。我們會在實現一節討論這個問題的一個解決
辦法。
9. 實現
下面是實現Abstract Factor模式的一些有用技術:
1) 將工廠作為單件一個應用中一般每個產品系列只需一個C o n c r e t e F a c t o r y的例項。因此
工廠通常最好實現為一個S i n g l e t o n(3 . 5)。
2) 建立產品A b s t r a c t F a c t o r y僅宣告一個建立產品的介面,真正建立產品是由
C o n c r e t e P r o d u c t子類實現的。最通常的一個辦法是為每一個產品定義一個工廠方法(參見
Factory Method(3 . 3))。一個具體的工廠將為每個產品重定義該工廠方法以指定產品。雖然
這樣的實現很簡單,但它卻要求每個產品系列都要有一個新的具體工廠子類,即使這些產品
系列的差別很小。
第3章建立型模式5 9

如果有多個可能的產品系列,具體工廠也可以使用P r o t o t y p e(3 . 4)模式來實現。具體工
廠使用產品系列中每一個產品的原型例項來初始化,且它通過複製它的原型來建立新的產品。
在基於原型的方法中,使得不是每個新的產品系列都需要一個新的具體工廠類。
此處是S m a l l t a l k中實現一個基於原型的工廠的方法。具體工廠在一個被稱為p a r t C a t a l o g的
字典中儲存將被複制的原型。方法m a k e:檢索該原型並複製它:
make : partName
^ (partCatalog at : partName) copy
具體工廠有一個方法用來向該目錄中增加部件。
addPart : partTemplate named : partName
partCatalog at : partName put : partTemplate
原型通過用一個符號標識它們,從而被增加到工廠中:
aFactory addPart : aPrototype named : #ACMEWidget
在將類作為第一類物件的語言中(例如S m a l l t a l k和O b j e c t i v e C),這個基於原型的方法可
能有所變化。你可以將這些語言中的一個類看成是一個退化的工廠,它僅建立一種產品。你
可以將類儲存在一個具體工廠中,這個具體工廠在變數中建立多個具體的產品,這很像原型。
這些類代替具體工廠建立了新的例項。你可以通過使用產品的類而不是子類初始化一個具體
工廠的例項,來定義一個新的工廠。這一方法利用了語言的特點,而純基於原型的方法是與
語言無關的。
像剛討論過的S m a l l t a l k中的基於原型的工廠一樣,基於類的版本將有一個唯一的例項變
量p a r t C a t a l o g,它是一個字典,它的主鍵是各部分的名字。p a r t C a t a l o g儲存產品的類而不是存
儲被複制的原型。方法m a k e:現在是這樣:
make : partName
^ (partCatalog at : partName) new
3) 定義可擴充套件的工廠A b s t r a c t F a c t o r y通常為每一種它可以生產的產品定義一個操作。產
品的種類被編碼在操作型構中。增加一種新的產品要求改變A b s t r a c t F a c t o r y的介面以及所有與
它相關的類。一個更靈活但不太安全的設計是給建立物件的操作增加一個引數。該引數指定
了將被建立的物件的種類。它可以是一個類識別符號、一個整數、一個字串,或其他任何可
以標識這種產品的東西。實際上使用這種方法, A b s t r a c t F a c t o r y只需要一個“ M a k e”操作和
一個指示要建立物件的種類的引數。這是前面已經討論過的基於原型的和基於類的抽象工廠
的技術。
C + +這樣的靜態型別語言與相比,這一變化更容易用在類似於S m a l l t a l k這樣的動態型別語
言中。僅當所有物件都有相同的抽象基類,或者當產品物件可以被請求它們的客戶安全的強
制轉換成正確型別時,你才能夠在C + +中使用它。Factory Method(3.3)的實現部分說明了怎樣
在C + +中實現這樣的引數化操作。
該方法即使不需要型別強制轉換,但仍有一個本質的問題:所有的產品將返回型別所給
定的相同的抽象介面返回給客戶。客戶將不能區分或對一個產品的類別進行安全的假定。如
果一個客戶需要進行與特定子類相關的操作,而這些操作卻不能通過抽象介面得到。雖然客
戶可以實施一個向下型別轉換( d o w n c a s t)(例如在C + +中用d y n a m i c _ c a s t),但這並不總是可
行或安全的,因為向下型別轉換可能會失敗。這是一個典型的高度靈活和可擴充套件介面的權衡
6 0 設計模式:可複用面向物件軟體的基礎

折衷。
10. 程式碼示例
我們將使用Abstract Factory模式建立我們在這章開始所討論的迷宮。
類M a z e F a c t o r y可以建立迷宮的元件。它建造房間、牆壁和房間之間的門。它可以用於一
個從檔案中讀取迷宮說明圖並建造相應迷宮的程式。或者它可以被用於一個隨機建造迷宮的
程式。建造迷宮的程式將M a z e F a c t o r y作為一個引數,這樣程式設計師就能指定要建立的房間、牆
壁和門等類。
回想一下建立一個由兩個房間和它們之間的門組成的小迷宮的成員函式C r e a t e M a z e。
C r e a t e M a z e對類名進行硬編碼,這使得很難用不同的元件建立迷宮。
這裡是一個以M a z e F a c t o r y為引數的新版本的CreateMaze ,它修改了以上缺點:
我們建立M a z e F a c t o r y的子類E n c h a n t e d M a z e F a c t o r y,這是一個建立施了魔法的迷宮的工
廠。E n c h a n t e d M a z e F a c t o r y將重定義不同的成員函式並返回R o o m,Wa l l等不同的子類。
第3章建立型模式6 1

現在假設我們想生成一個迷宮遊戲,在這個遊戲裡,每個房間中可以有一個。如果
這個爆炸,它將(至少)毀壞牆壁。我們可以生成一個R o o m的子類以明瞭是否有一個炸
彈在房間中以及該是否爆炸了。我們也將需要一個Wa l l的子類以明瞭對牆壁的損壞。我
們將稱這些類為R o o m Wi t h A B o m b和B o m b e d Wa l l。
我們將定義的最後一個類是B o m b e d M a z e F a c t o r y,它是M a z e F a c t o r y的子類,保證了牆壁
是B o m b e d Wa l l類的而房間是R o o m Wi t h A B o m b的。B o m b e d M a z e F a c t o r y僅需重定義兩個函式:
為建造一個包含的簡單迷宮,我們僅用B o m b e d M a z e F a c t o r y呼叫C r e a t e M a z e。
MazeGame game;
BombedMazeFactory factory;
g a m e . C r e a t e M a z e ( f a c t o r y ) ;
C r e a t e M a z e也可以接收一個E n c h a n t e d M a z e F a c t o r y例項來建造施了魔法的迷宮。
注意M a z e F a c t o r y僅是工廠方法的一個集合。這是最通常的實現Abstract Factory模式的方
式。同時注意M a z e F a c t o r y 不是一個抽象類;因此它既作為A b s t r a c t F a c t o r y 也作為
C o n c r e t e F a c t o r y。這是Abstract Factory 模式的簡單應用的另一個通常的實現。因為
M a z e F a c t o r y是一個完全由工廠方法組成的具體類,通過生成一個子類並重定義需要改變的操
作,它很容易生成一個新的M a z e F a c t o r y。
C r e a t e M a z e使用房間的S e t S i d e操作以指定它們的各面。如果它用一個B o m b e d M a z e F a c t o r y
建立房間,那麼該迷宮將由有B o m b e d Wa l l 面的R o o m Wi t h A B o m b 物件組成。如果
R o o m Wi t h A B o m b必須訪問一個B o m b e d Wa l l的與特定子類相關的成員,那麼它將不得不對它的
牆壁引用以進行從Wa l l *到B o m b e d Wa l l *的轉換。只要該引數確實是一個B o m b e d Wa l l,這個向
下型別轉換就是安全的,而如果牆壁僅由一個B o m b e d M a z e F a c t o r y建立就可以保證這一點。
當然,像S m a l l t a l k這樣的動態型別語言不需要向下型別轉換,但如果它們在應該是Wa l l
的子類的地方遇到一個Wa l l類可能會產生執行時刻錯誤。使用Abstract Factory建造牆壁,通
過確定僅有特定型別的牆壁可以被建立,從而有助於防止這些執行時刻錯誤。
讓我們考慮一個S m a l l t a l k版本的M a z e F a c t o r y,它僅有一個以要生成的物件種類為引數的
m a k e操作。此外,具體工廠儲存它所建立的產品的類。
首先,我們用S m a l l t a l k寫一個等價的C r e a t e M a z e:
6 2 設計模式:可複用面向物件軟體的基礎

正如我們在實現一節所討論, M a z e F a c t o r y僅需一個例項變數p a r t C a t a l o g來提供一個字典,
這個字典的主鍵為迷宮元件的類。也回想一下我們是如何實現m a k e :方法的:
現在我們可以建立一個M a z e F a c t o r y並用它來實現C r e a t e M a z e。我們將用類M a z e G a m e的
一個方法C r e a t e M a z e F a c t o r y來建立該工廠。
通過將不同的類與它們的主鍵相關聯,就可以建立一個B o m b e d M a z e F a c t o r y或E n c h a n t e d
M a z e F a c t o r y。例如,一個E n c h a n t e d M a z e F a c t o r y可以這樣被建立:
11. 已知應用
I n t e r Vi e w使用“K i t”字尾[ L i n 9 2 ]來表示A b s t r a c t F a c t o r y類。它定義Wi d g e t K i t和D i a l o g K i t
抽象工廠來生成與特定視感風格相關的使用者介面物件。I n t e r Vi e w還包括一個L a y o u t K i t,它根
據所需要的佈局生成不同的組成( c o m p o s i t i o n)物件。例如,一個概念上是水平的佈局根據
文件的定位(畫像或是風景)可能需要不同的組成物件。
E T + + [ W G M 8 8 ]使用Abstract Factory模式以達到在不同視窗系統(例如, X Wi n d o w s和
S u n Vi e w)間的可移植性。Wi n d o w S y s t e m抽象基類定義一些介面,來建立表示視窗系統資源
的物件(例如M a k e Wi n d o w、M a k e F o n t、M a k e C o l o r)。具體的子類為某個特定的視窗系統實
現這些介面。執行時刻, E T + +建立一個具體Wi n d o w S y s t e m子類的例項,以建立具體的系統
資源物件。
12. 相關模式
A b s t r a c t F a c t o r y類通常用工廠方法( Factory Method (3 . 3))實現,但它們也可以用
P r o t o t y p e實現。
一個具體的工廠通常是一個單件( S i n g l e t o n(3 . 5))。
3.2 BUILDER(生成器)—物件建立型模式
1. 意圖
將一個複雜物件的構建與它的表示分離,使得同樣的構建過程可以建立不同的表示。
第3章建立型模式6 3

2. 動機
一個RT F(Rich Text Format)文件交換格式的閱讀器應能將RT F轉換為多種正文格式。
該閱讀器可以將RT F文件轉換成普通A S C I I文字或轉換成一個能以互動方式編輯的正文視窗組
件。但問題在於可能轉換的數目是無限的。因此要能夠很容易實現新的轉換的增加,同時卻
不改變RT F閱讀器。
一個解決辦法是用一個可以將RT F轉換成另一種正文表示的Te x t C o n v e r t e r物件配置這個
RT F R e a d e r類。當RT F R e a d e r對RT F文件進行語法分析時,它使用Te x t C o n v e r t e r去做轉換。無
論何時RT F R e a d e r識別了一個RT F標記(或是普通正文或是一個RT F控制字),它都發送一個
請求給Te x t C o n v e r t e r去轉換這個標記。Te x t C o n v e r t e r物件負責進行資料轉換以及用特定格式
表示該標記,如下圖所示。
Te x t C o n v e r t的子類對不同轉換和不同格式進行特殊處理。例如,一個A S C I I C o n v e r t e r只
負責轉換普通文字,而忽略其他轉換請求。另一方面,一個Te X C o n v e r t e r將會為實現對所有
請求的操作,以便生成一個獲取正文中所有風格資訊的T E X表示。一個Te x t Wi d g e t C o n v e r t e r
將生成一個複雜的使用者介面物件以便使用者瀏覽和編輯正文。
每種轉換器類將建立和裝配一個複雜物件的機制隱含在抽象介面的後面。轉換器獨立於
閱讀器,閱讀器負責對一個RT F文件進行語法分析。
B u i l d e r模式描述了所有這些關係。每一個轉換器類在該模式中被稱為生成器( b u i l d e r),
而閱讀器則稱為導向器( d i r e c t o r)。在上面的例子中, B u i l d e r模式將分析文字格式的演算法
(即RT F文件的語法分析程式)與描述怎樣建立和表示一個轉換後格式的演算法分離開來。這使
我們可以重用RT F R e a d e r的語法分析演算法,根據RT F文件建立不同的正文表示—僅需使用不
同的Te x t C o n v e r t e r的子類配置該RT F R e a d e r即可。
3. 適用性
在以下情況使用B u i l d e r模式
• 當建立複雜物件的演算法應該獨立於該物件的組成部分以及它們的裝配方式時。
• 當構造過程必須允許被構造的物件有不同的表示時。
4. 結構
此模式結構如下頁上圖所示。
6 4 設計模式:可複用面向物件軟體的基礎

5. 參與者
• B u i l d e r(Te x t C o n v e r t e r)
— 為建立一個P r o d u c t物件的各個部件指定抽象介面。
• C o n c r e t e B u i l d e r(A S C I I C o n v e r t e r、Te X C o n v e r t e r、Te x t Wi d g e t C o n v e r t e r)
— 實現B u i l d e r的介面以構造和裝配該產品的各個部件。
— 定義並明確它所建立的表示。
— 提供一個檢索產品的介面(例如, G e t A S C I I Te x t和G e t Te x t Wi d g e t)。
• Director(RT F R e a d e r)
— 構造一個使用B u i l d e r介面的物件。
• P r o d u c t(A S C I I Te x t、Te X Te x t、Te x t Wi d g e t)
— 表示被構造的複雜物件。C o n c r e t e B u i l d e r建立該產品的內部表示並定義它的裝配過程。
— 包含定義組成部件的類,包括將這些部件裝配成最終產品的介面。
6. 協作
• 客戶建立D i r e c t o r物件,並用它所想要的B u i l d e r物件進行配置。
• 一旦產品部件被生成,導向器就會通知生成器。
• 生成器處理導向器的請求,並將部件新增到該產品中。
• 客戶從生成器中檢索產品。
下面的互動圖說明了B u i l d e r和D i r e c t o r是如何與一個客戶協作的。
7. 效果
這裡是B u i l d e r模式的主要效果:
第3章建立型模式6 5

1 ) 它使你可以改變一個產品的內部表示B u i l d e r物件提供給導向器一個構造產品的抽象
介面。該介面使得生成器可以隱藏這個產品的表示和內部結構。它同時也隱藏了該產品是如
何裝配的。因為產品是通過抽象介面構造的,你在改變該產品的內部表示時所要做的只是定
義一個新的生成器。
2) 它將構造程式碼和表示程式碼分開B u i l d e r模式通過封裝一個複雜物件的建立和表示方式
提高了物件的模組性。客戶不需要知道定義產品內部結構的類的所有資訊;這些類是不出現
在B u i l d e r介面中的。每個C o n c r e t e B u i l d e r包含了建立和裝配一個特定產品的所有程式碼。這些
程式碼只需要寫一次;然後不同的D i r e c t o r可以複用它以在相同部件集合的基礎上構作不同的
P r o d u c t。在前面的RT F例子中,我們可以為RT F格式以外的格式定義一個閱讀器,比如一個
S G M L R e a d e r,並使用相同的Te x t C o n v e r t e r生成S G M L文件的A S C I I Te x t、Te X Te x t和
Te x t Wi d g e t譯本。
3 ) 它使你可對構造過程進行更精細的控制B u i l d e r模式與一下子就生成產品的建立型模
式不同,它是在導向者的控制下一步一步構造產品的。僅當該產品完成時導向者才從生成器
中取回它。因此B u i l d e r介面相比其他建立型模式能更好的反映產品的構造過程。這使你可以
更精細的控制構建過程,從而能更精細的控制所得產品的內部結構。
8. 實現
通常有一個抽象的B u i l d e r類為導向者可能要求建立的每一個構件定義一個操作。這些操
作預設情況下什麼都不做。一個C o n c r e t e B u i l d e r類對它有興趣建立的構件重定義這些操作。
這裡是其他一些要考慮的實現問題:
1) 裝配和構造介面生成器逐步的構造它們的產品。因此B u i l d e r類介面必須足夠普遍,
以便為各種型別的具體生成器構造產品。
一個關鍵的設計問題在於構造和裝配過程的模型。構造請求的結果只是被新增到產品中,
通常這樣的模型就已足夠了。在RT F的例子中,生成器轉換下一個標記並將它新增到它已經
轉換了的正文中。
但有時你可能需要訪問前面已經構造了的產品部件。我們在程式碼示例一節所給出的M a z e
例子中, M a z e B u i l d e r介面允許你在已經存在的房間之間增加一扇門。像語法分析樹這樣自底
向上構建的樹型結構就是另一個例子。在這種情況下,生成器會將子結點返回給導向者,然
後導向者將它們回傳給生成者去建立父結點。
2) 為什麼產品沒有抽象類通常情況下,由具體生成器生成的產品,它們的表示相差是
如此之大以至於給不同的產品以公共父類沒有太大意思。在RT F例子中, A S C I I Te x t和
Te x t Wi d g e t物件不太可能有公共介面,它們也不需要這樣的介面。因為客戶通常用合適的具
體生成器來配置導向者,客戶處於的位置使它知道B u i l d e r的哪一個具體子類被使用和能相應
的處理它的產品。
3 ) 在B u i l d e r中卻省的方法為空C + +中,生成方法故意不宣告為純虛成員函式,而是把
它們定義為空方法,這使客戶只重定義他們所感興趣的操作。
9. 程式碼示例
我們將定義一個C r e a t e M a z e成員函式的變體,它以類M a z e B u i l d e r的一個生成器物件作為
引數。
M a z e B u i l d e r類定義下面的介面來建立迷宮:
6 6 設計模式:可複用面向物件軟體的基礎

該介面可以建立:1)迷宮。2)有一個特定房間號的房間。3)在有號碼的房間之間的門。
G e t M a z e操作返回這個迷宮給客戶。M a z e B u i l d e r的子類將重定義這些操作,返回它們所建立
的迷宮。
M a z e B u i l d e r的所有建造迷宮的操作預設時什麼也不做。不將它們定義為純虛擬函式是為了
便於派生類只重定義它們所感興趣的那些方法。
用M a z e B u i l d e r介面,我們可以改變C r e a t e M a z e成員函式,以生成器作為它的引數。
將這個C r e a t e M a z e版本與原來的相比,注意生成器是如何隱藏迷宮的內部表示的—即定
義房間、門和牆壁的那些類—以及這些部件是如何組裝成最終的迷宮的。有人可能猜測到
有一些類是用來表示房間和門的,但沒有跡象顯示哪個類是用來表示牆壁的。這就使得改變
一個迷宮的表示方式要容易一些,因為所有M a z e B u i l d e r的客戶都不需要被改變。
像其他建立型模式一樣, B u i l d e r模式封裝了物件是如何被建立的,在這個例子中是通過
M a z e B u i l d e r所定義的介面來封裝的。這就意味著我們可以重用M a z e B u i l d e r來建立不同種類的
迷宮。C r e a t e C o m p l e x M a z e操作給出了一個例子:
注意M a z e B u i l d e r自己並不建立迷宮;它的主要目的僅僅是為建立迷宮定義一個介面。它
主要為方便起見定義一些空的實現。M a z e B u i l d e r的子類做實際工作。
子類S t a n d a r d M a z e B u i l d e r是一個建立簡單迷宮的實現。它將它正在建立的迷宮放在變數
_ c u r r e n t M a z e中。
第3章建立型模式6 7

C o m m o n Wa l l是一個功能性操作,它決定兩個房間之間的公共牆壁的方位。
S t a n d a r d M a z e B u i l d e r的構造器只初始化了_ c u r r e n t M a z e。
B u i l d M a z e例項化一個M a z e,它將被其他操作裝配並最終返回給客戶(通過G e t M a z e)。
B u i l d R o o m操作建立一個房間並建造它周圍的牆壁:
為建造一扇兩個房間之間的門, S t a n d a r d M a z e B u i l d e r查詢迷宮中的這兩個房間並找到它
們相鄰的牆:
客戶現在可以用C r e a t e M a z e和S t a n d a r d M a z e B u i l d e r來建立一個迷宮:
我們本可以將所有的S t a n d a r d M a z e B u i l d e r操作放在M a z e中並讓每一個M a z e建立它自身。
但將M a z e變得小一些使得它能更容易被理解和修改,而且S t a n d a r d M a z e B u i l d e r易於從M a z e中
分離。更重要的是,將兩者分離使得你可以有多種M a z e B u i l d e r,每一種使用不同的房間、牆
壁和門的類。
6 8 設計模式:可複用面向物件軟體的基礎

一個更特殊的M a z e B u i l d e r是C o u n t i n g M a z e B u i l d e r。這個生成器根本不建立迷宮;它僅僅
對已被建立的不同種類的構件進行計數。
構造器初始化該計數器,而重定義了的M a z e B u i l d e r操作只是相應的增加計數。
下面是一個客戶可能怎樣使用C o u n t i n g M a z e B u i l d e r:
10. 已知應用
RT F轉換器應用來自E T + + [ W G M 8 8 ]。它的正文生成模組使用一個生成器處理以RT F格式
儲存的正文。
生成器在S m a l l t a l k - 8 0 [ P a r 9 0 ]中是一個通用的模式:
• 編譯子系統中的P a r s e r類是一個D i r e c t o r,它以一個P r o g r a m N o d e B u i l d e r物件作為引數。
每當P a r s e r物件識別出一個語法結構時,它就通知它的P r o g r a m N o d e B u i l d e r物件。當這
個語法分析器做完時,它向該生成器請求它生成的語法分析樹並將語法分析樹返回給客
戶。
第3章建立型模式6 9

• C l a s s B u i l d e r是一個生成器, C l a s s使用它為自己建立子類。在這個例子中,一個C l a s s既
是D i r e c t o r也是P r o d u c t。
• B y t e C o d e S t r e a m 是一個生成器,它將一個被編譯了的方法建立為位元組陣列。
B y t e C o d e S t r e a m不是B u i l d e r模式的標準使用,因為它生成的複雜物件被編碼為一個位元組
陣列,而不是正常的S m a l l t a l k物件。但B y t e C o d e S t r e a m的介面是一個典型的生成器,而
且將很容易用一個將程式表示為複合物件的不同的類來替換B y t e C o d e S t r e a m。
自適應通訊環境( Adaptive Communications Environment )中的服務配置者( S e r v i c e
C o n f i g u r a t o r)框架使用生成器來構造執行時刻動態連線到伺服器的網路服務構件[ S S 9 4 ]。這
些構件使用一個被L A L R(1)語法分析器進行語法分析的配置語言來描述。這個語法分析器
的語義動作對將資訊載入給服務構件的生成器進行操作。在這個例子中,語法分析器就是
D i r e c t o r。
11. 相關模式
Abstract Factory (3 . 1)與B u i l d e r相似,因為它也可以建立複雜物件。主要的區別是
B u i l d e r模式著重於一步步構造一個複雜物件。而Abstract Factory著重於多個系列的產品物件
(簡單的或是複雜的)。B u i l d e r在最後的一步返回產品,而對於Abstract Factory來說,產品是
立即返回的。
C o m p o s i t e(4 . 3)通常是用B u i l d e r生成的。
3.3 FACTORY METHOD(工廠方法)—物件建立型模式
1. 意圖
定義一個用於建立物件的介面,讓子類決定例項化哪一個類。Factory Method使一個類的
例項化延遲到其子類。
2. 別名
虛構造器( Virtual Constructor)
3. 動機
框架使用抽象類定義和維護物件之間的關係。這些物件的建立通常也由框架負責。
考慮這樣一個應用框架,它可以向用戶顯示多個文件。在這個框架中,兩個主要的抽象是
類A p p l i c a t i o n和D o c u m e n t。這兩個類都是抽象的,客戶必須通過它們的子類來做與具體應用相
關的實現。例如,為建立一個繪圖應用,我們定義類D r a w i n g A p p l i c a t i o n和D r a w i n g D o c u m e n t。
A p p l i c a t i o n類負責管理D o c u m e n t並根據需要建立它們—例如,當用戶從選單中選擇O p e n或
N e w的時候。
因為被例項化的特定D o c u m e n t子類是與特定應用相關的,所以A p p l i c a t i o n類不可能預測
到哪個D o c u m e n t子類將被例項化—A p p l i c a t i o n類僅知道一個新的文件何時應被建立,而不
知道哪一種D o c u m e n t將被建立。這就產生了一個尷尬的局面:框架必須例項化類,但是它只
知道不能被例項化的抽象類。
Factory Method模式提供了一個解決辦案。它封裝了哪一個D o c u m e n t子類將被建立的信
息並將這些資訊從該框架中分離出來,如下頁上圖所示。
A p p l i c a t i o n的子類重定義A p p l i c a t i o n的抽象操作C r e a t e D o c u m e n t以返回適當的D o c u m e n t
子類物件。一旦一個A p p l i c a t i o n子類例項化以後,它就可以例項化與應用相關的文件,而無
7 0 設計模式:可複用面向物件軟體的基礎

需知道這些文件的類。我們稱C r e a t e D o c u m e n t是一個工廠方法( f a c t o r y m e t h o d),因為它負
責“生產”一個物件。
4. 適用性
在下列情況下可以使用Factory Method模式:
• 當一個類不知道它所必須建立的物件的類的時候。
• 當一個類希望由它的子類來指定它所建立的物件的時候。
• 當類將建立物件的職責委託給多個幫助子類中的某一個,並且你希望將哪一個幫助子類
是代理者這一資訊區域性化的時候。
5. 結構
6. 參與者
• P r o d u c t( D o c u m e n t )
— 定義工廠方法所建立的物件的介面。
• C o n c r e t e P r o d u c t(M y D o c u m e n t)
— 實現P r o d u c t介面。
• C r e a t o r(A p p l i c a t i o n)
— 宣告工廠方法,該方法返回一個P r o d u c t型別的物件。C r e a t o r也可以定義一個工廠方
法的預設實現,它返回一個預設的C o n c r e t e P r o d u c t物件。
— 可以呼叫工廠方法以建立一個P r o d u c t物件。
• C o n c r e t e C r e a t o r(M y A p p l i c a t i o n)
— 重定義工廠方法以返回一個C o n c r e t e P r o d u c t例項。
7. 協作
• Creator依賴於它的子類來定義工廠方法,所以它返回一個適當的C o n c r e t e P r o d u c t例項。
8. 效果
第3章建立型模式7 1

工廠方法不再將與特定應用有關的類繫結到你的程式碼中。程式碼僅處理P r o d u c t介面;因此
它可以與使用者定義的任何C o n c r e t e P r o d u c t類一起使用。
工廠方法的一個潛在缺點在於客戶可能僅僅為了建立一個特定的C o n c r e t e P r o d u c t物件,
就不得不建立C r e a t o r的子類。當C r e a t o r子類不必需時,客戶現在必然要處理類演化的其他方
面;但是當客戶無論如何必須建立C r e a t o r的子類時,建立子類也是可行的。
下面是Factory Method模式的另外兩種效果:
1 ) 為子類提供掛鉤( h o o k) 用工廠方法在一個類的內部建立物件通常比直接建立物件
更靈活。Factory Method給子類一個掛鉤以提供物件的擴充套件版本。
在D o c u m e n t的例子中, D o c u m e n t類可以定義一個稱為C r e a t e F i l e D i a l o g的工廠方法,該方
法為開啟一個已有的文件建立預設的檔案對話方塊物件。D o c u m e n t的子類可以重定義這個工廠
方法以定義一個與特定應用相關的檔案對話方塊。在這種情況下,工廠方法就不再抽象了而是
提供了一個合理的預設實現。
2) 連線平行的類層次迄今為止,在我們所考慮的例子中,工廠方法並不往往只是被
C r e a t o r呼叫,客戶可以找到一些有用的工廠方法,尤其在平行類層次的情況下。
當一個類將它的一些職責委託給一個獨立的類的時候,就產生了平行類層次。考慮可以
被互動操縱的圖形;也就是說,它們可以用滑鼠進行伸展、移動,或者旋轉。實現這樣一些
互動並不總是那麼容易,它通常需要儲存和更新在給定時刻記錄操縱狀態的資訊,這個狀態
僅僅在操縱時需要。因此它不需要被儲存在圖形物件中。此外,當用戶操縱圖形時,不同的
圖形有不同的行為。例如,將直線圖形拉長可能會產生一個端點被移動的效果,而伸展正文
圖形則可能會改變行距。
有了這些限制,最好使用一個獨立的M a n i p u l a t o r物件實現互動並儲存所需要的任何與特
定操縱相關的狀態。不同的圖形將使用不同的M a n i p u l a t o r子類來處理特定的互動。得到的
M a n i p u l a t o r類層次與F i g u r e類層次是平行(至少部分平行),如下圖所示。
F i g u r e類提供了一個C r e a t e M a n i p u l a t o r工廠方法,它使得客戶可以建立一個與F i g u r e相對
應的M a n i p u l a t o r。F i g u r e子類重定義該方法以返回一個合適的M a n i p u l a t o r子類例項。做為一
種選擇, F i g u r e類可以實現C r e a t e M a n i p u l a t o r以返回一個預設的M a n i p u l a t o r例項,而F i g u r e子
類可以只是繼承這個預設實現。這樣的F i g u r e類不需要相應的M a n i p u l a t o r子類—因此該層次
只是部分平行的。
注意工廠方法是怎樣定義兩個類層次之間的連線的。它將哪些類應一同工作工作的資訊
區域性化了。
7 2 設計模式:可複用面向物件軟體的基礎

9. 實現
當應用Factory Method模式時要考慮下面一些問題:
1 ) 主要有兩種不同的情況Factory Method模式主要有兩種不同的情況: 1)第一種情況
是, C r e a t o r類是一個抽象類並且不提供它所宣告的工廠方法的實現。2)第二種情況是,
C r e a t o r是一個具體的類而且為工廠方法提供一個預設的實現。也有可能有一個定義了預設實
現的抽象類,但這不太常見。
第一種情況需要子類來定義實現,因為沒有合理的預設實現。它避免了不得不例項化不
可預見類的問題。在第二種情況中,具體的C r e a t o r主要因為靈活性才使用工廠方法。它所遵
循的準則是,“用一個獨立的操作建立物件,這樣子類才能重定義它們的建立方式。”這條準
則保證了子類的設計者能夠在必要的時候改變父類所例項化的物件的類。
2 ) 引數化工廠方法該模式的另一種情況使得工廠方法可以建立多種產品。工廠方法採
用一個標識要被建立的物件種類的引數。工廠方法建立的所有物件將共享P r o d u c t介面。在
D o c u m e n t的例子中,A p p l i c a t i o n可能支援不同種類的D o c u m e n t。你給C r e a t e D o c u m e n t傳遞一
個外部引數來指定將要建立的文件的種類。
圖形編輯框架Unidraw [VL90]使用這種方法來重構儲存在磁碟上的物件。U n i d r a w定義了
一個C r e a t o r類,該類擁有一個以類識別符號為引數的工廠方法C r e a t e。類識別符號指定要被例項
化的類。當U n i d r a w將一個物件存檔時,它首先寫類識別符號,然後是它的例項變數。當它從磁
盤中重構該物件時,它首先讀取的是類識別符號。
一旦類識別符號被讀取後,這個框架就將該識別符號作為引數,呼叫C r e a t e。C r e a t e到構造器
中查詢相應的類並用它例項化物件。最後, C r e a t e呼叫物件的R e a d操作,讀取磁碟上剩餘的
資訊並初始化該物件的例項變數。
一個引數化的工廠方法具有如下的一般形式,此處M y P r o d u c t和Yo u r P r o d u c t是P r o d u c t的
子類:
重定義一個引數化的工廠方法使你可以簡單而有選擇性的擴充套件或改變一個C r e a t o r生產的
產品。你可以為新產品引入新的識別符號,或可以將已有的識別符號與不同的產品相關聯。
例如,子類M y C r e a t o r可以交換M y P r o d u c t 和Yo u r P r o d u c t並且支援一個新的子類
T h e i r P r o d u c t:
第3章建立型模式7 3

注意這個操作所做的最後一件事是呼叫父類的C r e a t e。這是因為M y C r e a t o r : : C r e a t e僅在對
Y O U R S、M I N E和T H E I R S的處理上和父類不同。它對其他類不感興趣。因此M y C r e a t o r擴充套件
了所建立產品的種類,並且將除少數產品以外所有產品的建立職責延遲給了父類。
3) 特定語言的變化和問題不同的語言有助於產生其他一些有趣的變化和警告( c a v e a t)。
S m a l l t a l k程式通常使用一個方法返回被例項化的物件的類。C r e a t o r工廠方法可以使用這
個值去建立一個產品,並且C o n c r e t e C r e a t o r可以儲存甚至計算這個值。這個結果是對例項化
的C o n c r e t e P r o d u c t型別的一個更遲的繫結。
S m a l l t a l k版本的D o c u m e n t的例子可以在A p p l i c a t i o n中定義一個d o c u m e n t C l a s s方法。該方
法為例項化文件返回合適的D o c u m e n t類,其在M y A p p l i c a t i o n中的實現返回M y D o c u m e n t類。
這樣在類A p p l i c a t i o n中我們有
在類M y A p p l i c a t i o n中我們有
它把將被例項化的類M y D o c u m e n t返回給A p p l i c a t i o n。一個更靈活的類似於引數化工廠方
法的辦法是將被建立的類儲存為A p p l i c a t i o n的一個類變數。你用這種方法在改變產品時就無
需用到A p p l i c a t i o n的子類。
C + +中的工廠方法都是虛擬函式並且常常是純虛擬函式。一定要注意在C r e a t o r的構造器中不
要呼叫工廠方法—在C o n c r e t e C r e a t o r中該工廠方法還不可用。
只要你使用按需建立產品的訪問者操作,很小心地訪問產品,你就可以避免這一點。構
造器只是將產品初始化為0,而不是建立一個具體產品。訪問者返回該產品。但首先它要檢查
確定該產品的存在,如果產品不存在,訪問者就建立它。這種技術有時被稱為l a z y
i n i t i a l i z a t i o n。下面的程式碼給出了一個典型的實現:
4 ) 使用模板以避免建立子類正如我們已經提及的,工廠方法另一個潛在的問題是它們
可能僅為了建立適當的P r o d u c t物件而迫使你建立C r e a t o r子類。在C + +中另一個解決方法是提
供C r e a t o r的一個模板子類,它使用P r o d u c t類作為模板引數:
7 4 設計模式:可複用面向物件軟體的基礎

使用這個模板,客戶僅提供產品類—而不需要建立C r e a t o r的子類。
5 ) 命名約定使用命名約定是一個好習慣,它可以清楚地說明你正在使用工廠方法。例
如,M a c i n t o s h的應用框架MacApp [App89]總是宣告那些定義為工廠方法的抽象操作為C l a s s *
DoMakeClass( ),此處C l a s s是P r o d u c t類。
10. 程式碼示例
函式C r e a t e M a z e(第3章)建造並返回一個迷宮。這個函式存在的一個問題是它對迷宮、
房間、門和牆壁的類進行了硬編碼。我們將引入工廠方法以使子類可以選擇這些構件。首先
我們將在M a z e G a m e中定義工廠方法以建立迷宮、房間、牆壁和門物件:
每一個工廠方法返回一個給定型別的迷宮構件。M a z e G a m e提供一些預設的實現,它們返
回最簡單的迷宮、房間、牆壁和門。
現在我們可以用這些工廠方法重寫C r e a t e M a z e:
第3章建立型模式7 5

不同的遊戲可以建立M a z e G a m e的子類以特別指明一些迷宮的部件。M a z e G a m e子類可以
重定義一些或所有的工廠方法以指定產品中的變化。例如,一個B o m b e d M a z e G a m e可以重定
義產品R o o m和Wa l l以返回爆炸後的變體:
一個E n c h a n t e d M a z e G a m e變體可以像這樣定義:
11. 已知應用
工廠方法主要用於工具包和框架中。前面的文件例子是M a c A p p和E T + + [ W G M 8 8 ]中的一
個典型應用。操縱器的例子來自U n i d r a w。
Smalltalk-80 Model/Vi e w / C o n t r o l l e r框架中的類檢視( Class Vi e w)有一個建立控制器的
方法d e f a u l t C o n t r o l l e r,它有點類似於一個工廠方法[ P a r 9 0 ]。但是Vi e w的子類通過定義
d e f a u l t C o n t r o l l e r C l a s s 來指定它們預設的控制器的類。d e f a u l t C o n t r o l l e r C l a s s返回
d e f a u l t C o n t r o l l e r所建立例項的類,因此它才是真正的工廠方法,即子類應該重定義它。
S m a l l t a l k - 8 0中一個更為深奧的例子是由B e h a v i o r(用來表示類的所有物件的超類)定義
的工廠方法p a r s e r C l a s s。這使得一個類可以對它的原始碼使用一個定製的語法分析器。例如,
7 6 設計模式:可複用面向物件軟體的基礎

一個客戶可以定義一個類S Q L P a r s e r來分析嵌入了S Q L語句的類的原始碼。B e h a v i o r類實現了
p a r s e r C l a s s,返回一個標準的Smalltalk Parser類。一個包含嵌入S Q L語句的類重定義了該方法
(以類方法的形式)並返回S Q L P a r s e r類。
IONA Te c h n o l o g i e s的Orbix ORB系統[ I O N 9 4 ]在物件給一個遠端物件引用傳送請求時,使
用Factory Method生成一個適當型別的代理(參見P r o x y(4 . 7))。Factory Method使得易於替
換預設代理。比如說,可以用一個使用客戶端快取記憶體的代理來替換。
12. 相關模式
Abstract Factory(3 . 1)經常用工廠方法來實現。Abstract Factory模式中動機一節的例子
也對Factory Method進行了說明。
工廠方法通常在Template Methods(5 . 1 0)中被呼叫。在上面的文件例子中,N e w D o c u m e n t
就是一個模板方法。
P r o t o t y p e s(3 . 4)不需要建立C r e a t o r的子類。但是,它們通常要求一個針對P r o d u c t類的
I n i t i a l i z e操作。C r e a t o r使用I n i t i a l i z e來初始化物件。而Factory Method不需要這樣的操作。
3.4 PROTOTYPE(原型)—物件建立型模式
1. 意圖
用原型例項指定建立物件的種類,並且通過拷貝這些原型建立新的物件。
2. 動機
你可以通過定製一個通用的圖形編輯器框架和增加一些表示音符、休止符和五線譜的新
物件來構造一個樂譜編輯器。這個編輯器框架可能有一個工具選擇板用於將這些音樂物件加
到樂譜中。這個選擇板可能還包括選擇、移動和其他操縱音樂物件的工具。使用者可以點選四
分音符工具並使用它將四分音符加到樂譜中。或者他們可以使用移動工具在五線譜上上下移
動一個音符,從而改變它的音調。
我們假定該框架為音符和五線譜這樣的圖形構件提供了一個抽象的G r a p h i c s類。此外,為
定義選擇板中的那些工具,還提供一個抽象類To o l。該框架還為一些建立圖形物件例項並將
它們加入到文件中的工具預定義了一個G r a p h i c To o l子類。
但G r a p h i c To o l給框架設計者帶來一個問題。音符和五線譜的類特定於我們的應用,而
G r a p h i c To o l類卻屬於框架。G r a p h i c To o l不知道如何建立我們的音樂類的例項,並將它們新增
到樂譜中。我們可以為每一種音樂物件建立一個G r a p h i c To o l的子類,但這樣會產生大量的子
類,這些子類僅僅在它們所初始化的音樂物件的類別上有所不同。我們知道物件複合是比創
建子類更靈活的一種選擇。問題是,該框架怎麼樣用它來引數化G r a p h i c To o l的例項,而這些
例項是由G r a p h i c類所支援建立的。
解決辦法是讓G r a p h i c To o l通過拷貝或者“克隆”一個G r a p h i c子類的例項來建立新的
G r a p h i c,我們稱這個例項為一個原型。G r a p h i c To o l將它應該克隆和新增到文件中的原型作為
引數。如果所有G r a p h i c子類都支援一個C l o n e操作,那麼G r a p h i c To o l可以克隆所有種類的
G r a p h i c,如下頁上圖所示。
因此在我們的音樂編輯器中,用於建立個音樂物件的每一種工具都是一個用不同原型進
行初始化的G r a p h i c To o l例項。通過克隆一個音樂物件的原型並將這個克隆新增到樂譜中,每
個G r a p h i c To o l例項都會產生一個音樂物件。
第3章建立型模式7 7

我們甚至可以進一步使用P r o t o t y p e模式來減少類的數目。我們使用不同的類來表示全音
符和半音符,但可能不需要這麼做。它們可以是使用不同點陣圖和時延初始化的相同的類的實
例。一個建立全音符的工具就是這樣的G r a p h i c To o l,它的原型是一個被初始化成全音符的
M u s i c a l N o t e。這可以極大的減少系統中類的數目,同時也更易於在音樂編輯器中增加新的音
符。
3. 適用性
當一個系統應該獨立於它的產品建立、構成和表示時,要使用P r o t o t y p e模式;以及
• 當要例項化的類是在執行時刻指定時,例如,通過動態裝載;或者
• 為了避免建立一個與產品類層次平行的工廠類層次時;或者
• 當一個類的例項只能有幾個不同狀態組合中的一種時。建立相應數目的原型並克隆它們
可能比每次用合適的狀態手工例項化該類更方便一些。
4. 結構
5. 參與者
• P r o t o t y p e(G r a p h i c)
— 宣告一個克隆自身的介面。
• C o n c r e t e P r o t o t y p e(S t a ff、W h o l e N o t e、H a l f N o t e)
— 實現一個克隆自身的操作。
• C l i e n t(G r a p h i c To o l)
— 讓一個原型克隆自身從而建立一個新的物件。
7 8 設計模式:可複用面向物件軟體的基礎

6. 協作
• 客戶請求一個原型克隆自身。
7. 效果
P r o t o t y p e有許多和Abstract Factory(3 . 1)和B u i l d e r(3 . 2)一樣的效果:它對客戶隱藏了
具體的產品類,因此減少了客戶知道的名字的數目。此外,這些模式使客戶無需改變即可使
用與特定應用相關的類。
下面列出P r o t o t y p e模式的另外一些優點。
1 ) 執行時刻增加和刪除產品P r o t o t y p e允許只通過客戶註冊原型例項就可以將一個新的
具體產品類併入系統。它比其他建立型模式更為靈活,因為客戶可以在執行時刻建立和刪除
原型。
2 ) 改變值以指定新物件高度動態的系統允許你通過物件複合定義新的行為—例如,通
過為一個物件變數指定值—並且不定義新的類。你通過例項化已有類並且將這些例項註冊
為客戶物件的原型,就可以有效定義新類別的物件。客戶可以將職責代理給原型,從而表現
出新的行為。
這種設計使得使用者無需程式設計即可定義新“類”。實際上,克隆一個原型類似於例項化一個
類。P r o t o t y p e模式可以極大的減少系統所需要的類的數目。在我們的音樂編輯器中,一個
G r a p h i c To o l類可以建立無數種音樂物件。
3) 改變結構以指定新物件許多應用由部件和子部件來建立物件。例如電路設計編輯器
就是由子電路來構造電路的。為方便起見,這樣的應用通常允許你例項化複雜的、使用者定
義的結構,比方說,一次又一次的重複使用一個特定的子電路。
P r o t o t y p e模式也支援這一點。我們僅需將這個子電路作為一個原型增加到可用的電路元
素選擇板中。只要複合電路物件將C l o n e實現為一個深拷貝( deep copy),具有不同結構的電
路就可以是原型了。
4 ) 減少子類的構造Factory Method(3 . 3)經常產生一個與產品類層次平行的C r e a t o r類
層次。P r o t o t y p e模式使得你克隆一個原型而不是請求一個工廠方法去產生一個新的物件。因
此你根本不需要C r e a t o r類層次。這一優點主要適用於像C + +這樣不將類作為一級類物件的語
言。像S m a l l t a l k和Objective C這樣的語言從中獲益較少,因為你總是可以用一個類物件作為
生成者。在這些語言中,類物件已經起到原型一樣的作用了。
5) 用類動態配置應用一些執行時刻環境允許你動態將類裝載到應用中。在像C + +這樣的
語言中,P r o t o t y p e模式是利用這種功能的關鍵。
一個希望建立動態載入類的例項的應用不能靜態引用類的構造器。而應該由執行環境在
載入時自動建立每個類的例項,並用原型管理器來註冊這個例項(參見實現一節)。這樣應用
就可以向原型管理器請求新裝載的類的例項,這些類原本並沒有和程式相連線。E T + +應用框
架[ W G M 8 8 ]有一個執行系統就是使用這一方案的。
P r o t o t y p e的主要缺陷是每一個P r o t o t y p e的子類都必須實現C l o n e操作,這可能很困難。例
如,當所考慮的類已經存在時就難以新增C l o n e操作。當內部包括一些不支援拷貝或有迴圈引
用的物件時,實現克隆可能也會很困難的。
8. 實現
第3章建立型模式7 9

這樣的應用反映了C o m p o s i t e(4 . 3)和D e c o r a t o r(4 . 4)模式。
因為在像C + +這樣的靜態語言中,類不是物件,並且執行時刻只能得到很少或者得不到任
何型別資訊,所以P r o t o t y p e特別有用。而在S m a l l t a l k或Objective C這樣的語言中P r o t o t y p e就
不是那麼重要了,因為這些語言提供了一個等價於原型的東西(即類物件)來建立每個類的
例項。P r o t o t y p e模式在像S e l f [ U S 8 7 ]這樣基於原型的語言中是固有的,所有物件的建立都是通
過克隆一個原型實現的。
當實現原型時,要考慮下面一些問題:
1 ) 使用一個原型管理器當一個系統中原型數目不固定時(也就是說,它們可以動態創
建和銷燬),要保持一個可用原型的登錄檔。客戶不會自己來管理原型,但會在登錄檔中儲存
和檢索原型。客戶在克隆一個原型前會向登錄檔請求該原型。我們稱這個登錄檔為原型管理
器(prototype manager)。
原型管理器是一個關聯儲存器( associative store),它返回一個與給定關鍵字相匹配的原
型。它有一些操作可以用來通過關鍵字註冊原型和解除註冊。客戶可以在執行時更改甚或瀏
覽這個登錄檔。這使得客戶無需編寫程式碼就可以擴充套件並得到系統清單。
2 ) 實現克隆操作P r o t o t y p e模式最困難的部分在於正確實現C l o n e操作。當物件結構包含
迴圈引用時,這尤為棘手。
大多數語言都對克隆物件提供了一些支援。例如, S m a l l t a l k提供了一個c o p y的實現,它
被所有O b j e c t的子類所繼承。C + +提供了一個拷貝構造器。但這些設施並不能解決“淺拷貝和
深拷貝”問題[ G R 8 3 ]。也就是說,克隆一個物件是依次克隆它的例項變數呢,或者還是由克
隆物件和原物件共享這些變數?
淺拷貝簡單並且通常也足夠了,它是S m a l l t a l k所預設提供的。C + +中的預設拷貝構造器實
現按成員拷貝,這意味著在拷貝的和原來的物件之間是共享指標的。但克隆一個結構複雜的
原型通常需要深拷貝,因為複製物件和原物件必須相互獨立。因此你必須保證克隆物件的構
件也是對原型的構件的克隆。克隆迫使你決定如果所有東西都被共享了該怎麼辦。
如果系統中的物件提供了S a v e和L o a d操作,那麼你只需通過儲存物件和立刻載入物件,
就可以為C l o n e操作提供一個預設實現。S a v e操作將該物件儲存在記憶體緩衝區中,而L o a d則通
過從該緩衝區中重構這個物件來建立一個複本。
3) 初始化克隆物件當一些客戶對克隆物件已經相當滿意時,另一些客戶將會希望使用
他們所選擇的一些值來初始化該物件的一些或是所有的內部狀態。一般來說不可能在C l o n e操
作中傳遞這些值,因為這些值的數目由於原型的類的不同而會有所不同。一些原型可能需要
多個初始化引數,另一些可能什麼也不要。在C l o n e操作中傳遞引數會破壞克隆介面的統一
性。
可能會這樣,原型的類已經為(重)設定一些關鍵的狀態值定義好了操作。如果這樣的
話,客戶在克隆後馬上就可以使用這些操作。否則,你就可能不得不引入一個I n i t i a l i z e操作
(參見程式碼示例一節),該操作使用初始化引數並據此設定克隆物件的內部狀態。注意深拷貝
C l o n e操作—一些複製在你重新初始化它們之前可能必須要被刪除掉(刪除可以顯式地做也
可以在I n i t i a l i z e內部做)。
9. 程式碼示例
我們將定義M a z e F a c t o r y(3 . 1)的子類M a z e P r o t o t y p e F a c t o r y。該子類將使用它要