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

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


第5章行為模式
行為模式涉及到演算法和物件間職責的分配。行為模式不僅描述物件或類的模式,還描述
它們之間的通訊模式。這些模式刻劃了在執行時難以跟蹤的複雜的控制流。它們將你的注意
力從控制流轉移到物件間的聯絡方式上來。
行為類模式使用繼承機制在類間分派行為。本章包括兩個這樣的模式。其中Te m p l a t e
M e t h o d(5 . 1 0)較為簡單和常用。模板方法是一個演算法的抽象定義,它逐步地定義該演算法,
每一步呼叫一個抽象操作或一個原語操作,子類定義抽象操作以具體實現該演算法。另一種行
為類模式是I n t e r p r e t e r(5 . 3)。它將一個文法表示為一個類層次,並實現一個直譯器作為這些
類的例項上的一個操作。
行為物件模式使用物件複合而不是繼承。一些行為物件模式描述了一組對等的物件怎樣
相互協作以完成其中任一個物件都無法單獨完成的任務。這裡一個重要的問題是對等的物件
如何互相瞭解對方。對等物件可以保持顯式的對對方的引用,但那會增加它們的耦合度。在
極端情況下,每一個物件都要了解所有其他的物件。M e d i a t o r(5 . 5)在對等物件間引入一個
m e d i a t o r物件以避免這種情況的出現。m e d i a t o r提供了鬆耦合所需的間接性。
Chain of Responsibility(5.1)提供更鬆的耦合。它讓你通過一條候選物件鏈隱式的向一個對
象傳送請求。根據執行時刻情況任一候選者都可以響應相應的請求。候選者的數目是任意的,
你可以在執行時刻決定哪些候選者參與到鏈中。
O b s e r v e r ( 5 . 7 )模式定義並保持物件間的依賴關係。典型的O b s e r v e r的例子是Smalltalk 中的
模型/檢視/控制器,其中一旦模型的狀態發生變化,模型的所有檢視都會得到通知。
其他的行為物件模式常將行為封裝在一個物件中並將請求指派給它。S t r a t e g y ( 5 . 9 )模式將
演算法封裝在物件中,這樣可以方便地指定和改變一個物件所使用的演算法。C o m m a n d ( 5 . 2 )模式
將請求封裝在物件中,這樣它就可作為引數來傳遞,也可以被儲存在歷史列表裡,或者以其
他方式使用。S t a t e ( 5 . 8 )模式封裝一個物件的狀態,使得當這個物件的狀態物件變化時,該對
象可改變它的行為。Vi s i t o r ( 5 . 11 )封裝分佈於多個類之間的行為,而I t e r a t o r ( 5 . 4 )則抽象了訪問
和遍歷一個集合中的物件的方式。
5.1 CHAIN OF RESPONSIBILITY(職責鏈)-物件行為型模式
1. 意圖
使多個物件都有機會處理請求,從而避免請求的傳送者和接收者之間的耦合關係。將這
些物件連成一條鏈,並沿著這條鏈傳遞該請求,直到有一個物件處理它為止。
2. 動機
考慮一個圖形使用者介面中的上下文有關的幫助機制。使用者在介面的任一部分上點選就可
以得到幫助資訊,所提供的幫助依賴於點選的是介面的哪一部分以及其上下文。例如,對話
框中的按鈕的幫助資訊就可能和主視窗中類似的按鈕不同。如果對那一部分介面沒有特定的
幫助資訊,那麼幫助系統應該顯示一個關於當前上下文的較一般的幫助資訊-比如說,整個
對話方塊。
因此很自然地,應根據普遍性( g e n e r a l i t y )即從最特殊到最普遍的順序來組織幫助資訊。
而且,很明顯,在這些使用者介面物件中會有一個物件來處理幫助請求;至於是哪一個物件則取
決於上下文以及可用的幫助具體到何種程度。
這兒的問題是提交幫助請求的物件(如按鈕)並不明確知道誰是最終提供幫助的物件。我們
要有一種辦法將提交幫助請求的物件與可能提供幫助資訊的物件解耦( d e c o u p l e )。Chain of
R e s p o n s i b i l i t y模式告訴我們應該怎麼做。
這一模式的想法是,給多個物件處理一個請求的機會,從而解耦傳送者和接受者。該請
求沿物件鏈傳遞直至其中一個物件處理它,如下圖所示。
從第一個物件開始,鏈中收到請求的物件要麼親自處理它,要麼轉發給鏈中的下一個候
選者。提交請求的物件並不明確地知道哪一個物件將會處理它-我們說該請求有一個隱式的
接收者(implicit receiver)。
假設使用者在一個標有“P r i n t” 的按鈕視窗元件上單擊幫助,而該按鈕包含在一個
P r i n t D i a l o g的例項中,該例項知道它所屬的應用物件(見前面的物件框圖)。下面的互動框圖
(diagram) 說明了幫助請求怎樣沿鏈傳遞:
在這個例子中,既不是aPrintButton 也不是aPrintDialog 處理該請求;它一直被傳遞給
a n A p p l i c a t i o n,anApplication 處理它或忽略它。提交請求的客戶不直接引用最終響應它的對
象。
要沿鏈轉發請求,並保證接收者為隱式的( i m p l i c i t ),每個在鏈上的物件都有一致的處理請
求和訪問鏈上後繼者的介面。例如,幫助系統可定義一個帶有相應的HandleHelp 操作的
H e l p H a n d l e r類。HelpHandler 可為所有候選物件類的父類,或者它可被定義為一個混入
(m i x i n)類。這樣想處理幫助請求的類就可將HelpHandler 作為其一個父類,如下頁上圖所示。
按鈕、對話方塊,和應用類都使用HelpHandler 操作來處理幫助請求。H e l p H a n d l e r的
HandleHelp 操作預設的是將請求轉發給後繼。子類可重定義這一操作以在適當的情況下提供
幫助;否則它們可使用預設實現轉發該請求。
3. 適用性
在以下條件下使用Responsibility 鏈:
• 有多個的物件可以處理一個請求,哪個物件處理該請求執行時刻自動確定。
• 你想在不明確指定接收者的情況下,向多個物件中的一個提交一個請求。
• 可處理一個請求的物件集合應被動態指定。
4. 結構
一個典型的物件結構可能如下圖所示:
5. 參與者
• H a n d l e r(如H e l p H a n d l e r)
- 定義一個處理請求的介面。
- (可選) 實現後繼鏈。
• C o n c r e t e H a n d l e r(如P r i n t B u t t o n和P r i n t D i a l o g)
- 處理它所負責的請求。
- 可訪問它的後繼者。
- 如果可處理該請求,就處理之;否則將該請求轉發給它的後繼者。
• C l i e n t
- 向鏈上的具體處理者( C o n c r e t e H a n d l e r )物件提交請求。
6. 協作
• 當客戶提交一個請求時,請求沿鏈傳遞直至有一個ConcreteHandler 物件負責處理它。
7. 效果
Responsibility 鏈有下列優點和缺點( l i a b i l i t i e s ) :
1 ) 降低耦合度該模式使得一個物件無需知道是其他哪一個物件處理其請求。物件僅需
知道該請求會被“正確”地處理。接收者和傳送者都沒有對方的明確的資訊,且鏈中的物件
不需知道鏈的結構。
結果是,職責鏈可簡化物件的相互連線。它們僅需保持一個指向其後繼者的引用,而不
需保持它所有的候選接受者的引用。
2) 增強了給物件指派職責( R e s p o n s i b i l i t y )的靈活性當在物件中分派職責時,職責鏈給你
更多的靈活性。你可以通過在執行時刻對該鏈進行動態的增加或修改來增加或改變處理一個
請求的那些職責。你可以將這種機制與靜態的特例化處理物件的繼承機制結合起來使用。
3) 不保證被接受既然一個請求沒有明確的接收者,那麼就不能保證它一定會被處理-
該請求可能一直到鏈的末端都得不到處理。一個請求也可能因該鏈沒有被正確配置而得不到
處理。
8. 實現
下面是在職責鏈模式中要考慮的實現問題:
1) 實現後繼者鏈有兩種方法可以實現後繼者鏈。
a) 定義新的連結(通常在H a n d l e r中定義,但也可由ConcreteHandlers 來定義)。
b) 使用已有的連結。
我們的例子中定義了新的連結,但你常常可使用已有的物件引用來形成後繼者鏈。例如,
在一個部分-整體層次結構中,父構件引用可定義一個部件的後繼者。視窗元件( Wi d g e t)
結構可能早已有這樣的連結。C o m p o s i t e(4 . 3)更詳細地討論了父構件引用。
當已有的連結能夠支援你所需的鏈時,完全可以使用它們。這樣你不需要明確定義連結,
而且可以節省空間。但如果該結構不能反映應用所需的職責鏈,那麼你必須定義額外的連結。
2) 連線後繼者如果沒有已有的引用可定義一個鏈,那麼你必須自己引入它們。這種情
況下H a n d l e r 不僅定義該請求的介面,通常也維護後繼連結。這樣H a n d l e r 就提供了
H a n d l e R e q u e s t 的預設實現: H a n d l e R e q u e s t 向後繼者( 如果有的話) 轉發請求。如果
ConcreteHandler 子類對該請求不感興趣,它不需重定義轉發操作,因為它的預設實現進行無
條件的轉發。
此處為一個H e l p H a n d l e r基類,它維護一個後繼者連結:
3) 表示請求可以有不同的方法表示請求。最簡單的形式,比如在H a n d l e H e l p的例子中,
請求是一個硬編碼的(hard-coded) 操作呼叫。這種形式方便而且安全,但你只能轉發H a n d l e r
類定義的固定的一組請求。
另一選擇是使用一個處理函式,這個函式以一個請求碼(如一個整型常數或一個字串)為
引數。這種方法支援請求數目不限。唯一的要求是傳送方和接受方在請求如何編碼問題上應
達成一致。
這種方法更為靈活,但它需要用條件語句來區分請求程式碼以分派請求。另外,無法用類
型安全的方法來傳遞請求引數,因此它們必須被手工打包和解包。顯然,相對於直接呼叫一
個操作來說它不太安全。
為解決引數傳遞問題,我們可使用獨立的請求物件來封裝請求引數。R e q u e s t類可明確地
描述請求,而新型別的請求可用它的子類來定義。這些子類可定義不同的請求引數。處理者
必須知道請求的型別(即它們正使用哪一個R e q u e s t子類)以訪問這些引數。
為標識請求,R e q u e s t可定義一個訪問器( a c c e s s o r )函式以返回該類的識別符號。或者,如果
實現語言支援的話,接受者可使用執行時的型別資訊。
以下為一個分派函式的框架( s k e t c h ),它使用請求物件標識請求。定義於基類R e q u e s t中的
G e t K i n d操作識別請求的型別:
子類可通過重定義H a n d l e R e q u e s t擴充套件該分派函式。子類只處理它感興趣的請求;其他的
請求被轉發給父類。這樣就有效的擴充套件了(而不是重寫) H a n d l e R e q u e s t操作。例如,一個
E x t e n d e d H a n d l e r子類擴充套件了M y H a n d l e r版本的H a n d l e R e q u e s t :
4) 在S m a l l t a l k中自動轉發你可以使用Smalltalk 中的d o e s N o t U n d e r s t a n d機制轉發請求。
沒有相應方法的訊息被doseNotUnderstand 的實現捕捉(trap in),此實現可被重定義,從而可
向一個物件的後繼者轉發該訊息。這樣就不需要手工實現轉發;類僅處理它感興趣的請求,
而依賴doesNotUnderstand 轉發所有其他的請求。
9. 程式碼示例
下面的例子舉例說明了在一個像前面描述的線上幫助系統中,職責鏈是如何處理請求的。
幫助請求是一個顯式的操作。我們將使用在視窗元件層次中的已有的父構件引用來在鏈中的
視窗元件間傳遞請求,並且我們將在H a n d l e r類中定義一個引用以在鏈中的非視窗元件間傳遞
幫助請求。
HelpHandler 類定義了處理幫助請求的介面。它維護一個幫助主題(預設值為空),並保持
對幫助處理物件鏈中它的後繼者的引用。關鍵的操作是H a n d l e H e l p,它可被子類重定義。
HasHelp 是一個輔助操作,用於檢查是否有一個相關的幫助主題。
所有的視窗元件都是Wi d g e t抽象類的子類。Wi d g e t是HelpHandler 的子類,因為所有的用
戶介面元素都可有相關的幫助。(我們也可以使用另一種基於混入類的實現方式)
在我們的例子中,按鈕是鏈上的第一個處理者。B u t t o n類是Wi d g e t類的子類。Button 構
造函式有兩個引數: 對包含它的視窗元件的引用和其自身的幫助主題。
B u t t o n版本的H a n d l e H e l p首先測試檢查其自身是否有幫助主題。如果開發者沒有定義一個
幫助主題,就用H e l p H a n d l e r中的H a n d l e H e l p操作將該請求轉發給它的後繼者。如果有幫助主
題,那麼就顯示它,並且搜尋結束。
D i a l o g實現了一個類似的策略,只不過它的後繼者不是一個視窗元件而是任意的幫助請求
處理物件。在我們的應用中這個後繼者將是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不是
H e l p H a n d l e r的直接子類。當一個幫助請求傳遞到這一層時,該應用可提供關於該應用的一般
性的資訊,或者它可以提供一系列不同的幫助主題。
下面的程式碼建立並連線這些物件。此處的對話方塊涉及列印,因此這些物件被賦給與列印
相關的主題。
我們可對鏈上的任意物件呼叫H a n d l e H e l p以觸發相應的幫助請求。要從按鈕物件開始搜
索,只需對它呼叫H a n d l e H e l p :
b u t t o n - > H a n d l e H e l p ( ) ;
在這種情況下,按鈕會立即處理該請求。注意任何H e l p H a n d l e r類都可作為D i a l o g的後繼
者。此外,它的後繼者可以被動態地改變。因此不管對話方塊被用在何處,你都可以得到它正
確的與上下文相關的幫助資訊。
10. 已知應用
許多類庫使用職責鏈模式處理使用者事件。對H a n d l e r類它們使用不同的名字,但思想是一樣
的:當用戶點選滑鼠或按鍵盤,一個事件產生並沿鏈傳播。MacApp[App89] 和E T + + [ W G M 8 8 ]
稱之為“事件處理者”,S y m a n t e c的T C L庫[ S y m 9 3 b ]稱之為“B u r e a u c r a t”,而N e X T的A p p K i t命
名為“R e s p o n d e r”。
圖形編輯器框架U n i d r a w定義了“命令” C o m m a n d物件,它封裝了發給C o m p o n e n t和
C o m p o n e n t Vi e w物件[ V L 9 0 ]的請求。一個構件或構件檢視可解釋一個命令以進行一個操作,
這裡“命令”就是請求。這對應於在實現一節中描述的“物件作為請求” 的方法。構件和構
件檢視可以組織為層次式的結構。一個構件或構件檢視可將命令解釋轉發給它的父構件,而
父構件依次可將它轉發給它的父構件,如此類推,就形成了一個職責鏈。
E T + +使用職責鏈來處理圖形的更新。當一個圖形物件必須更新它的外觀的一部分時,調
用I n v a l i d a t e R e c t操作。一個圖形物件自己不能處理I n v a l i d a t e R e c t,因為它對它的上下文了解
不夠。例如,一個圖形物件可被包裝在一些類似滾動條( S c r o l l e r s )或放大器( Z o o m e r s )的物件中,
這些物件變換它的座標系統。那就是說,物件可被滾動或放大以至它有一部分在視區外。因
此預設的I n v a l i d a t e R e c t的實現轉發請求給包裝的容器物件。轉發鏈中的最後一個物件是一個
視窗( Wi n d o w )例項。當視窗收到請求時,保證失效矩形被正確變換。視窗通知視窗系統介面
並請求更新,從而處理I n v a l i d a t e R e c t。
11. 相關模式
職責鏈常與C o m p o s i t e(4 . 3)一起使用。這種情況下,一個構件的父構件可作為它的後繼。
5.2 COMMAND(命令)-物件行為型模式
1. 意圖
將一個請求封裝為一個物件,從而使你可用不同的請求對客戶進行引數化;對請求排隊
或記錄請求日誌,以及支援可撤消的操作。
2. 別名
動作( A c t i o n ),事務( Tr a n s a c t i o n )
3. 動機
有時必須向某物件提交請求,但並不知道關於被請求的操作或請求的接受者的任何資訊。
例如,使用者介面工具箱包括按鈕和選單這樣的物件,它們執行請求響應使用者輸入。但工具箱
不能顯式的在按鈕或選單中實現該請求,因為只有使用工具箱的應用知道該由哪個物件做哪
個操作。而工具箱的設計者無法知道請求的接受者或執行的操作。
命令模式通過將請求本身變成一個物件來使工具箱物件可向未指定的應用物件提出請求。
這個物件可被儲存並像其他的物件一樣被傳遞。這一模式的關鍵是一個抽象的C o m m a n d類,
它定義了一個執行操作的介面。其最簡單的形式是一個抽象的E x e c u t e操作。具體的C o m m a n d
子類將接收者作為其一個例項變數,並實現E x e c u t e操作,指定接收者採取的動作。而接收者
有執行該請求所需的具體資訊。
用C o m m a n d物件可很容易的實現選單( M e n u),每一選單中的選項都是一個選單項
(M e n u I t e m)類的例項。一個A p p l i c a t i o n類建立這些選單和它們的選單項以及其餘的使用者介面。
該A p p l i c a t i o n類還跟蹤使用者已開啟的D o c u m e n t物件。
該應用為每一個選單項配置一個具體的C o m m a n d子類的例項。當用戶選擇了一個選單項
時,該M e n u I t e m物件呼叫它的C o m m a n d物件的E x e c u t e方法,而E x e c u t e執行相應操作。
M e n u I t e m物件並不知道它們使用的是C o m m a n d的哪一個子類。C o m m a n d子類裡存放著請求的
接收者,而E x c u t e操作將呼叫該接收者的一個或多個操作。
例如,P a s t e C o m m a n d支援從剪貼簿向一個文件( D o c u m e n t )貼上正文。P a s t e C o m m a n d的接
收者是一個文件物件,該物件是例項化時提供的。E x e c u t e操作將呼叫該D o c u m e n t的P a s t e操
作。
而O p e n C o m m a n d的E x e c u t e操作卻有所不同:它提示使用者輸入一個文件名,建立一個相應

的文件物件,將其入作為接收者的應用物件中,並開啟該文件。
有時一個M e n u I t e m需要執行一系列命令。例如,使一個頁面按正常大小居中的M e n u I t e m
可由一個C e n t e r D o c u m e n t C o m m a n d物件和一個N o r m a l S i z e C o m m a n d物件構建。因為這種需將
多條命令串接起來的情況很常見,我們定義一個M a c r o C o m m a n d類來讓一個M e n u I t e m執行任
意數目的命令。M a c r o C o m m a n d是一個具體的C o m m a n d子類,它執行一個命令序列。
M a c r o C o m m a n d沒有明確的接收者,而序列中的命令各自定義其接收者。
請注意這些例子中C o m m a n d模式是怎樣解耦呼叫操作的物件和具有執行該操作所需資訊
的那個物件的。這使我們在設計使用者介面時擁有很大的靈活性。一個應用如果想讓一個選單
與一個按鈕代表同一項功能,只需讓它們共享相應具體C o m m a n d子類的同一個例項即可。我
們還可以動態地替換C o m m a n d物件,這可用於實現上下文有關的選單。我們也可通過將幾個
命令組成更大的命令的形式來支援命令指令碼(command scripting)。所有這些之所以成為可能乃
是因為提交一個請求的物件僅需知道如何提交它,而不需知道該請求將會被如何執行。
4. 適用性
當你有如下需求時,可使用C o m m a n d模式:
• 像上面討論的M e n u I t e m物件那樣,抽象出待執行的動作以引數化某物件。你可用過程
語言中的回撥(c a l l b a c k)函式表達這種引數化機制。所謂回撥函式是指函式先在某處
註冊,而它將在稍後某個需要的時候被呼叫。C o m m a n d模式是回撥機制的一個面向對
象的替代品。
• 在不同的時刻指定、排列和執行請求。一個C o m m a n d物件可以有一個與初始請求無關
的生存期。如果一個請求的接收者可用一種與地址空間無關的方式表達,那麼就可將負
責該請求的命令物件傳送給另一個不同的程序並在那兒實現該請求。
• 支援取消操作。C o m m a n d的E x c u t e操作可在實施操作前將狀態儲存起來,在取消操作時
這個狀態用來消除該操作的影響。C o m m a n d介面必須新增一個U n e x e c u t e操作,該操作
取消上一次E x e c u t e呼叫的效果。執行的命令被儲存在一個歷史列表中。可通過向後和
向前遍歷這一列表並分別呼叫U n e x e c u t e和E x e c u t e來實現重數不限的“取消”和“重
做”。
• 支援修改日誌,這樣當系統崩潰時,這些修改可以被重做一遍。在C o m m a n d介面中添
加裝載操作和儲存操作,可以用來保持變動的一個一致的修改日誌。從崩潰中恢復的過
程包括從磁碟中重新讀入記錄下來的命令並用E x e c u t e操作重新執行它們。
• 用構建在原語操作上的高層操作構造一個系統。這樣一種結構在支援事務( t r a n s a c t i o n )
的資訊系統中很常見。一個事務封裝了對資料的一組變動。C o m m a n d模式提供了對事
務進行建模的方法。C o m m a n d有一個公共的介面,使得你可以用同一種方式呼叫所有
的事務。同時使用該模式也易於新增新事務以擴充套件系統。
5. 結構
6. 參與者
• C o m m a n d
- 宣告執行操作的介面。
• C o n c r e t e C o m m a n d ( P a s t e C o m m a n d,O p e n C o m m a n d )
- 將一個接收者物件綁定於一個動作。
- 呼叫接收者相應的操作,以實現E x e c u t e。
• C l i e n t ( A p p l i c t i o n )
- 建立一個具體命令物件並設定它的接收者。
• Invoker ( M e n u I t e m )
- 要求該命令執行這個請求。
• R e c e i v e r ( D o c u m e n t,A p p l i c a t i o n )
- 知道如何實施與執行一個請求相關的操作。任何類都可能作為一個接收者。
7. 協作
• C l i e n t建立一個C o n c r e t e C o m m a n d物件並指定它的R e c e i v e r物件。
• 某I n v o k e r物件儲存該C o n c r e t e C o m m a n d物件。

• 該I n v o k e r通過呼叫C o m m a n d物件的E x e c u t e操作來提交一個請求。若該命令是可撤消的,
C o n c r e t e C o m m a n d就在執行E x c u t e操作之前儲存當前狀態以用於取消該命令。
• ConcreteCommand物件對呼叫它的R e c e i v e r的一些操作以執行該請求。
下圖展示了這些物件之間的互動。它說明了C o m m a n d是如何將呼叫者和接收者(以及它執
行的請求)解耦的。
8. 效果
C o m m a n d模式有以下效果:
1) Command模式將呼叫操作的物件與知道如何實現該操作的物件解耦。
2) Command是頭等的物件。它們可像其他的物件一樣被操縱和擴充套件。
3) 你可將多個命令裝配成一個複合命令。例如是前面描述的M a c r o C o m m a n d類。一般說
來,複合命令是C o m p o s i t e模式的一個例項。
4) 增加新的C o m m a n d很容易,因為這無需改變已有的類。
9. 實現
實現C o m m a n d模式時須考慮以下問題:
1) 一個命令物件應達到何種智慧程度命令物件的能力可大可小。一個極端是它僅確定
一個接收者和執行該請求的動作。另一極端是它自己實現所有功能,根本不需要額外的接收
者物件。當需要定義與已有的類無關的命令,當沒有合適的接收者,或當一個命令隱式地知
道它的接收者時,可以使用後一極端方式。例如,建立另一個應用視窗的命令物件本身可能
和任何其他的物件一樣有能力建立該視窗。在這兩個極端間的情況是命令物件有足夠的資訊
可以動態的找到它們的接收者。
2 ) 支援取消( u n d o)和重做( r e d o) 如果C o m m a n d提供方法逆轉( r e v e r s e )它們操作的執
行( 例如U n e x e c u t e 或U n d o 操作) ,就可支援取消和重做功能。為達到這個目的,
C o n c r e t e C o m m a n d類可能需要儲存額外的狀態資訊。這個狀態包括:
• 接收者物件,它真正執行處理該請求的各操作。
• 接收者上執行操作的引數。
• 如果處理請求的操作會改變接收者物件中的某些值,那麼這些值也必須先儲存起來。接
收者還必須提供一些操作,以使該命令可將接收者恢復到它先前的狀態。
若應用只支援一次取消操作,那麼只需儲存最近一次被執行的命令。而若要支援多級的
取消和重做,就需要有一個已被執行命令的歷史表列(history list),該表列的最大長度決定了
取消和重做的級數。歷史表列儲存了已被執行的命令序列。向後遍歷該表列並逆向執行
( r e v e r s e - e x e c u t i n g )命令是取消它們的結果;向前遍歷並執行命令是重執行它們。
有時可能不得不將一個可撤消的命令在它可以被放入歷史列表中之前先拷貝下來。這是
因為執行原來的請求的命令物件將在稍後執行其他的請求。如果命令的狀態在各次呼叫之間
會發生變化,那就必須進行拷貝以區分相同命令的不同調用。
例如,一個刪除選定物件的刪除命令( D e l e t e C o m m a n d )在它每次被執行時,必須儲存不同
的物件集合。因此該刪除命令物件在執行後必須被拷貝,並且將該拷貝放入歷史表列中。如
果該命令的狀態在執行時從不改變,則不需要拷貝,而僅需將一個對該命令的引用放入歷史
表列中。在放入歷史表列中之前必須被拷貝的那些C o m m a n d起著原型(參見P r o t o t y p e模式
(3 . 4))的作用。
3 ) 避免取消操作過程中的錯誤積累在實現一個可靠的、能保持原先語義的取消/重做機
制時,可能會遇到滯後影響問題。由於命令重複的執行、取消執行,和重執行的過程可能會
積累錯誤,以至一個應用的狀態最終偏離初始值。這就有必要在C o m m a n d中存入更多的資訊
以保證這些物件可被精確地復原成它們的初始狀態。這裡可使用M e m e n t o模式(5 . 6)來讓該
C o m m a n d訪問這些資訊而不暴露其他物件的內部資訊。
4) 使用C + +模板對( 1 )不能被取消( 2 )不需要引數的命令,我們可使用C + +模板來實現,
這樣可以避免為每一種動作和接收者都建立一個C o m m a n d子類。我們將在程式碼示例一節說明
這種做法。
10. 程式碼示例
此處所示的C + +程式碼給出了動機一節中的C o m m a n d類的實現的大致框架。我們將定義
O p e n C o m m a n d、P a s t e C o m m a n d和M a c r o C o m m a n d。首先是抽象的C o m m a n d類:
O p e n C o m m a n d開啟一個名字由使用者指定的文件。注意O p e n C o m m a n d的構造器需要一個
A p p l i c a t i o n物件作為引數。A s k U s e r是一個提示使用者輸入要開啟的文件名的實現例程。
P a s t e C o m m a n d需要一個D o c u m e n t物件作為其接收者。該接收者將作為一個引數給
P a s t e C o m m a n d的構造器。
對於簡單的不能取消和不需引數的命令, 可以用一個類模板來引數化該命令的接收者。我
們將為這些命令定義一個模板子類SimpleCommand. 用R e c e i v e r型別引數化S i m p l e C o m m a n d,
並維護一個接收者物件和一個動作之間的繫結,而這一動作是用指向一個成員函式的指標儲存
的。
構造器儲存接收者和對應例項變數中的動作。E x e c u t e操作實施接收者的這個動作。
為建立一個呼叫M y c l a s s類的一個例項上的A c t i o n的C o m m a n d物件, 僅需如下程式碼:
記住, 這一方案僅適用於簡單命令。更復雜的命令不僅要維護它們的接收者,而且還要登
記引數,有時還要儲存用於取消操作的狀態。此時就需要定義一個C o m m a n d的子類。
M a c r o C o m m a n d管理一個子命令序列,它提供了增加和刪除子命令的操作。這裡不需要
顯式的接收者,因為這些子命令已經定義了它們各自的接收者。
M a c r o C o m m a n d的關鍵是它的E x e c u t e成員函式。它遍歷所有的子命令並呼叫其各自的
E x e c u t e操作。
注意,如果M a c r o C o m m a n d實現取消操作, 那麼它的子命令必須以相對於E x e c u t e的實現相
反的順序執行各子命令的取消操作。
最後, MacroCommand必須提供管理它的子命令的操作。M a c r o C o m m a n d也負責刪除它的
子命令。
11. 已知應用
可能最早的命令模式的例子出現在L i e b e r m a n [ L i e 8 5 ]的一篇論文中。M a c A p p [ A p p 8 9 ]使實
現可撤消操作的命令這一說法被普遍接受。而E T + + [ W G M 8 8 ],I n t e r Vi e w s [ L C I + 9 2 ],和
U n i d r a w [ V L 9 0 ]也都定義了符合C o m m a n d模式的類。I n t e r Vi e w s定義了一個A c t i o n抽象類,它
提供命令功能。它還定義了一個A c t i o n C a l l b a c k模板,這個模板以A c t i o n方法為引數, 可自動
生成C o m m a n d子類。
T H I N K類庫[ S y m 9 3 b ]也使用C o m m a n d模式支援可撤消的操作。T H I N K中的命令被稱為
“任務”( Ta s k s )。任務物件沿著一個Chain of Responsiblity(5 . 1)傳遞以供消費( c o n s u m p t i o n )。
U n i d r a w的命令物件很特別,它的行為就像是一個訊息。一個U n i d r a w命令可被送給另一
個物件去解釋,而解釋的結果因接收的物件而異。此外, 接收者可以委託另一個物件來進行解
釋,典型的情況的是委託給一個較大的結構中(比如在一個職責鏈中)接收者的父構件。這樣,
U n i d r a w命令的接收者是計算出來的而不是預先儲存的。U n i d r a w的解釋機制依賴於執行時的
型別資訊。
C o p l i e n在C + + [ C o p 9 2 ]中描述了C + +中怎樣實現f u n c t o r s。F u n c t o r s是一種實際上是函式的
物件。他通過過載函式呼叫操作符(operator( ))達到了一定程度的使用透明性。命令模式不同,
它著重於維護接收者和函式(即動作)之間的繫結, 而不僅是維護一個函式。
12. 相關模式
C o m p o s i t e模式(4 . 3)可被用來實現巨集命令。
M e m e n t o模式(5 . 6)可用來保持某個狀態,命令用這一狀態來取消它的效果。
在被放入歷史表列前必須被拷貝的命令起到一種原型( 3 . 4 )的作用。
5.3 INTERPRETER(直譯器)-類行為型模式
1. 意圖
給定一個語言,定義它的文法的一種表示,並定義一個直譯器,這個直譯器使用該表示
來解釋語言中的句子。
2. 動機
如果一種特定型別的問題發生的頻率足夠高, 那麼可能就值得將該問題的各個例項表述為
一個簡單語言中的句子。這樣就可以構建一個直譯器, 該直譯器通過解釋這些句子來解決該問
題。
例如,搜尋匹配一個模式的字串是一個常見問題。正則表示式是描述字串模式的一
種標準語言。與其為每一個的模式都構造一個特定的演算法,不如使用一種通用的搜尋演算法來
解釋執行一個正則表示式,該正則表示式定義了待匹配字串的集合。
直譯器模式描述瞭如何為簡單的語言定義一個文法, 如何在該語言中表示一個句子, 以及
如何解釋這些句子。在上面的例子中, 本設計模式描述瞭如何為正則表示式定義一個文法, 如
何表示一個特定的正則表示式, 以及如何解釋這個正則表示式。
考慮以下文法定義正則表示式:
符號e x p r e s s i o n是開始符號, literal是定義簡單字的終結符。
直譯器模式使用類來表示每一條文法規則。在規則右邊的符號是這些類的例項變數。上面的
文法用五個類表示: 一個抽象類R e g u l a r E x p r e s s i o n和它四個子類L i t e r a l E x p r e s s i o n、A l t e r n a t i o n
E x p r e s s i o n、S e q u e n c e E x p r e s s i o n和R e p e t i t i o n E x p r e s s i o n後三個類定義的變數代表子表示式。
每個用這個文法定義的正則表示式都被表示為一個由這些類的例項構成的抽象語法樹。
例如, 抽象語法樹:
表示正則表示式:
raining & (dogs | cats) *
如果我們為R e g u l a r E x p r e s s i o n的每一子類都定義解釋( I n t e r p r e t )操作,那麼就得到了為這
些正則表示式的一個直譯器。直譯器將該表示式的上下文做為一個引數。上下文包含輸入字
符串和關於目前它已有多少已經被匹配等資訊。為匹配輸入字串的下一部分,每一
R e g u l a r E x p r e s s i o n的子類都在當前上下文的基礎上實現解釋操作( I n t e r p r e t )。例如,
• LiteralExpression將檢查輸入是否匹配它定義的字( l i t e r a l )。
• AlternationExpression將檢查輸入是否匹配它的任意一個選擇項。
• RepetitionExpression將檢查輸入是否含有多個它所重複的表示式。
等等。
3. 適用性
當有一個語言需要解釋執行, 並且你可將該語言中的句子表示為一個抽象語法樹時,可使
用直譯器模式。而當存在以下情況時該模式效果最好:
• 該文法簡單對於複雜的文法, 文法的類層次變得龐大而無法管理。此時語法分析程式生
成器這樣的工具是更好的選擇。它們無需構建抽象語法樹即可解釋表示式, 這樣可以節
省空間而且還可能節省時間。
• 效率不是一個關鍵問題最高效的直譯器通常不是通過直接解釋語法分析樹實現的, 而是
首先將它們轉換成另一種形式。例如,正則表示式通常被轉換成狀態機。但即使在這種
情況下, 轉換器仍可用直譯器模式實現, 該模式仍是有用的。
4. 結構(見下頁圖)
5. 參與者
• A b s t r a c t E x p r e s s i o n (抽象表示式,如R e g u l a r E x p r e s s i o n )
- 宣告一個抽象的解釋操作,這個介面為抽象語法樹中所有的節點所共享。
• Te r m i n a l E x p r e s s i o n (終結符表示式,如L i t e r a l E x p r e s s i o n )
- 實現與文法中的終結符相關聯的解釋操作。
- 一個句子中的每個終結符需要該類的一個例項。
• N o n t e r m i n a l E x p r e s s i o n (非終結符表示式,如AlternationExpression, Repetition-
Expression, SequenceExpressions)
- 對文法中的每一條規則R ::= R1R2. . . Rn都需要一個N o n t e r m i n a l E x p r e s s i o n類。
- 為從R1到Rn的每個符號都維護一個A b s t r a c t E x p r e s s i o n型別的例項變數。
- 為文法中的非終結符實現解釋( I n t e r p r e t )操作。解釋( I n t e r p r e t )一般要遞迴地呼叫表示
R1到Rn的那些物件的解釋操作。
• C o n t e x t(上下文)
- 包含直譯器之外的一些全域性資訊。
• C l i e n t(客戶)
- 構建(或被給定) 表示該文法定義的語言中一個特定的句子的抽象語法樹。該抽象語
法樹由N o n t e r m i n a l E x p r e s s i o n和Te r m i n a l E x p r e s s i o n的例項裝配而成。
- 呼叫解釋操作。
6. 協作
• C l i e n t構建(或被給定)一個句子, 它是N o n t e r m i n a l E x p r e s s i o n和Te r m i n a l E x p r e s s i o n的例項
的一個抽象語法樹. 然後初始化上下文並呼叫解釋操作。
• 每一非終結符表示式節點定義相應子表示式的解釋操作。而各終結符表示式的解釋操作
構成了遞迴的基礎。
• 每一節點的解釋操作用上下文來儲存和訪問直譯器的狀態。
7. 效果
直譯器模式有下列的優點和不足:
1) 易於改變和擴充套件文法因為該模式使用類來表示文法規則, 你可使用繼承來改變或擴充套件
該文法。已有的表示式可被增量式地改變,而新的表示式可定義為舊錶達式的變體。
2) 也易於實現文法定義抽象語法樹中各個節點的類的實現大體類似。這些類易於直接
編寫,通常它們也可用一個編譯器或語法分析程式生成器自動生成。
3) 複雜的文法難以維護直譯器模式為文法中的每一條規則至少定義了一個類(使用B N F定
義的文法規則需要更多的類)。因此包含許多規則的文法可能難以管理和維護。可應用其他的設
計模式來緩解這一問題。但當文法非常複雜時, 其他的技術如語法分析程式或編譯器生成器更為
合適。
4) 增加了新的解釋表示式的方式直譯器模式使得實現新表示式“計算”變得容易。例如,
你可以在表示式類上定義一個新的操作以支援優美列印或表示式的型別檢查。如果你經常建立
新的解釋表示式的方式, 那麼可以考慮使用Vi s i t o r ( 5 . 11 )模式以避免修改這些代表文法的類。
8. 實現
I n t e r p r e t e r和C o m p o s i t e(4 . 3)模式在實現上有許多相通的地方。下面是I n t e r p r e t e r所要考
慮的一些特殊問題:
1) 建立抽象語法樹直譯器模式並未解釋如何建立一個抽象的語法樹。換言之, 它不涉及
語法分析。抽象語法樹可用一個表驅動的語法分析程式來生成,也可用手寫的(通常為遞迴下
降法) 語法分析程式建立,或直接由C l i e n t提供。
2) 定義解釋操作並不一定要在表示式類中定義解釋操作。如果經常要建立一種新的解
釋器, 那麼使用Vi s i t o r(5 . 11)模式將解釋放入一個獨立的“訪問者” 物件更好一些。例如,
一個程式設計語言的會有許多在抽象語法樹上的操作,比如型別檢查、優化、程式碼生成,等
等。恰當的做法是使用一個訪問者以避免在每一個類上都定義這些操作。
3) 與F l y w e i g h t模式共享終結符在一些文法中, 一個句子可能多次出現同一個終結符。此
時最好共享那個符號的單個拷貝。計算機程式的文法是很好的例子-每個程式變數在整個
程式碼中將會出現多次。在動機一節的例子中, 一個句子中終結符dog (由L i t e r a l E x p r e s s i o n類描
述)也可出現多次。
終結節點通常不儲存關於它們在抽象語法樹中位置的資訊。在解釋過程中,任何它們所
需要的上下文資訊都由父節點傳遞給它們。因此在共享的(內部的)狀態和傳入的(外部的)狀態
區分得很明確, 這就用到了F l y w e i g h t(4 . 6)模式。
例如,dog LiteralExpression的每一例項接收一個包含目前已匹配子串資訊的上下文。且
每一個這樣的L i t e r a l E x p r e s s i o n在它的解釋操作中做同樣一件事(它檢查輸入的下一部分是否
包含一個d o g)無論該例項出現在語法樹的哪個位置。
9. 程式碼示例
下面是兩個例子。第一個是S m a l l t a l k中一個完整的的例子, 用於檢查一個序列是否匹配一
個正則表示式。第二個是一個用於求布林表示式的值的C + +程式。
正則表示式匹配器檢查一個字串是否屬於一個正則表示式定義的語言。正則表示式用
下列文法定義:
該文法對動機一節中的例子略做修改。因為符號“*” 在S m a l l t a l k中不能作為字尾運算
符。因此我們用r e p e a t取代之。例如, 正則表示式:
( ( 'dog' | 'cat' ) repeat &'weather')
匹配輸入字串“dog dog cat weather”。
為實現這個匹配器, 我們定義在( 5 . 3)頁描述的五個類。類S e q u e n c e E x p r e s s i o n包含例項
變數expression 1和expression 2作為它在抽象語法樹中的子結點; A l t e r n a t i o n E x p r e s s i o n用例項
變數altercative 1和altercative 2中儲存它的選擇支;而R e p e t i t i o n E x p r e s s i o n在它的例項變數
r e p e t i t i o n中儲存它所重複的表示式。L i t e r a l E x p r e s s i o n有一個c o m p o n e n t s例項變數,它儲存了
一系列物件(可能為一些字元)。這些表示必須匹配輸入序列的字串(literal string)。
m a t c h :操作實現了該正則表示式的一個直譯器。定義抽象語法樹的每一個類都實現了這一
操作。它將i n p u t S t a t e作為一個引數, 表示匹配程序的當前狀態,也就是讀入的部分輸入字串。
這一狀態由一個輸入流集刻畫, 表示該正則表示式目前所能接收的輸入集(當前已識別出
的輸入流, 這大致等價於記錄等價的有限自動機可能處於的所有狀態)。
當前狀態對r e p e a t操作最為重要。例如, 如果正則表示式為:
'a' repeat
那麼直譯器可匹配“ a”, “a a”, “a a a”, 等等。如果它是
'a' repeat & 'bc'
那麼可以匹配“a b c”, “a a b c”, “a a a b c”, 等等. 但如果正則表示式是
'a' repeat & 'abc'
那麼用子表示式“‘a’ r e p e a t” 匹配輸入“a a b c” 將產生兩個輸入流, 一個匹配了輸入
的一個字元, 而另一個匹配了兩個字元。只有接受一個字元的那個流會匹配剩餘的“a b c”。
現在我們考慮m a t c h的定義: 對每一個類定義相應的正則表示式。S e q u e n c e E x p r e s s i o n匹配
其序列中的每一個子表示式。通常它將從它的i n p u t S t a t e中刪除輸入流。
一個A l t e r n a t i o n E x p r e s s i o n會返回一個狀態, 該狀態由兩個選擇項的狀態的並組成。
A l t e r n a t i o n E x p r e s s i o n的match 的定義是
R e p e t i t i o n E x p r e s s i o n的m a t c h :操作尋找儘可能多的可匹配的狀態:
它的輸出通常比它的輸入包含更多的狀態, 因為R e p e t i t i o n E x p r e s s i o n可匹配輸入的重複體
的一次、兩次或多次出現。而輸出狀態要表示所有這些可能性以允許隨後的正則表示式的元
素決定哪一個狀態是正確的。
最後, LiteralExpression的m a t c h :對每一可能的輸入流匹配它的組成部分。它僅保留那些獲
得匹配的輸入流:
其中n e x t Av a i l a b l e :訊息推進輸入流(即讀入文字)。這是唯一一個推進輸入流的m a t c h :操
作。注意返回的狀態包含的是輸入流的拷貝, 這就保證匹配一個l i t e r a l不會改變輸入流。這一
點很重要,因為每個A l t e r n a t i o n E x p r e s s i o n的選擇項看到的應該是相同的輸入流。
現在我們已經定義了組成抽象語法樹的各個類,下面說明怎樣構建語法樹。我們犯不著
為正則表示式寫一個語法分析程式,而只要在R e g u l a r E x p r e s s i o n類上定義一些操作,就可以
“計算”一個S m a l l t a l k表示式,得到的結果就是對應於該正則表示式的一棵抽象語法樹。這使
我們可以把S m a l l t a l k內建編譯器當作一個正則表示式的語法分析程式來使用。
為構建抽象語法樹, 我們需要將“|”、“r e p e a t”,和“&”定義為R e g u l a r E x p r e s s i o n上的操
作。這些操作在R e g u l a r E x p r e s s i o n類中定義如下:
a s R E x p操作將把l i t e r a l s轉化為R e g u l a r E x p r e s s i o n。這些操作在類S t r i n g中定義:
如果我們在類層次的更高層( S m a l l t a l k中的SequenceableCollection, Smalltalk/V 中的
I n d e x e d C o l l e c i o t n )中定義這些操作, 那麼象A r r a y和O r d e r e d C o l l e c t i o n這樣的類也有這些操作的
定義,這就使得正則表示式可以匹配任何型別的物件序列。
第二個例子是在C + +中實現的對布林表示式進行操作和求值。在這個語言中終結符是布林
變數, 即常量t r u e和f a l s e。非終結符表示包含運算子and, or和n o t的布林表示式。文法定義如下:
為簡單起見, 我們忽略了操作符的優先次序且假定由構造該語法樹的物件負責處理這件事。
這裡我們定義布林表示式上的兩個操作。第一個操作是求值( e v a l u a t e ),即在一個上下文
中求一個布林表示式的值,當然,該上下文必須為每個變數都賦以一個“真”或“假”的布
爾值。第二個操作是替換(replace), 即用一個表示式來替換一個變數以產生一個新的布林表達
式。替換操作說明了直譯器模式不僅可以用於求表示式的值,而且還可用作其它用途。在這
個例子中, 它就被用來對錶達式本身進行操作。
此處我們僅給出BooleanExp, Va r i a b l e E x p和A n d E x p類的細節。類O r E x p和N o t E x p與
A n d E x p相似。C o n s t a n t類表示布林常量。
B o o l e a n E x p為所有定義一個布林表示式的類定義了一個介面:
類C o n t e x t定義從變數到布林值的一個對映, 這些布林值我們可用C + +中的常量t r u e和f a l s e
來表示。C o n t e x t有以下介面:
一個Va r i a b l e E x p表示一個有名變數:
構造器將變數的名字作為引數:
求一個變數的值, 返回它在當前上下文中的值。
拷貝一個變數返回一個新的Va r i a b l e E x p :
1 6 8 設計模式:可複用面向物件軟體的基礎

在用一個表示式替換一個變數時, 我們檢查該待替換變數是否就是本物件代表的變數:
A n d E x p表示由兩個布林表示式與操作得到的表示式。
一個A n d E x p的值求是它的運算元的值的邏輯“與”。
A n d E x p的C o p y和R e p l a c e操作將遞迴呼叫它的運算元的C o p y和R e p l a c e操作:
現在我們可以定義布林表示式
並對給定的以t r u e或f a l s e賦值的x和y求這個表示式值:
對x和y的這一賦值,求得該表示式值為t r u e。要對其它賦值情況求該表示式的值僅需改變
上下文物件即可。
最後, 我們可用一個新的表示式替換變數y,並重新求值:
這個例子說明了直譯器模式一個很重要的特點: 可以用多種操作來“解釋” 一個句子。
在為B o o l e a n E x p定義的三種操作中, Evaluate 最切合我們關於一個直譯器應該做什麼的想法
-即, 它解釋一個程式或表示式並返回一個簡單的結果。但是,替換操作也可被視為一個解
釋器。這個直譯器的上下文是被替換變數的名字和替換它的表示式, 而它的結果是一個新的表
達式。甚至拷貝也可被視為一個上下文為空的直譯器。將替換和拷貝視為直譯器可能有點怪,
因為它們僅僅是樹上的基本操作。Vi s i t o r ( 5 . 11 )中的例子說明了這三個操作都可以被重新組織
為獨立的“直譯器”訪問者, 從而顯示了它們之間深刻的相似性。
直譯器模式不僅僅是分佈在一個使用C o m p o s i t e ( 4 . 3 )模式的類層次上的操作。我們之所以
認為E v a l u a t e是一個直譯器, 是因為我們認為B o o l e a n E x p類層次表示一個語言。對於一個用於
表示汽車部件裝配的類層次,即使它也使用複合模式,我們還是不太可能將We i g h t和C o p y這
樣的操作視為直譯器,因為我們不會把汽車部件當作一個語言。這是一個看問題的角度問題;
如果我們真有“汽車部件語言”的語法, 那麼也許可以認為在那些部件上的操作是以某種方式
解釋該語言。
10. 已知應用
直譯器模式在使用面嚮物件語言實現的編譯器中得到了廣泛應用, 如S m a l l t a l k編譯器。
S P E C Ta l k使用該模式解釋輸入檔案格式的描述[ S z a 9 2 ]。Q O C A約束-求解工具使用它對約束
進行計算[ H H M V 9 2 ]。
在最寬泛的概念下(即, 分佈在基於C o m p o s i t e ( 4 . 3 )模式的類層次上的一種操作), 幾乎每個
使用複合模式的系統也都使用瞭解釋器模式。但一般只有在用一個類層次來定義某個語言時,
才強調使用直譯器模式。
11. 相關模式
C o m p o s i t e模式(4 . 3): 抽象語法樹是一個複合模式的例項。
F l y w e i g h t模式(4 . 6):說明了如何在抽象語法樹中共享終結符。
I t e r a t o r(5 . 4):直譯器可用一個迭代器遍歷該結構。
Vi s i t o r(5 . 11):可用來在一個類中維護抽象語法樹中的各節點的行為。
5.4 ITERATOR(迭代器)-物件行為型模式
1. 意圖
提供一種方法順序訪問一個聚合物件中各個元素, 而又不需暴露該物件的內部表示。
2. 別名
遊標(C u r s o r)。
3. 動機
一個聚合物件, 如列表(list), 應該提供一種方法來讓別人可以訪問它的元素,而又不需暴
露它的內部結構. 此外,針對不同的需要,可能要以不同的方式遍歷這個列表。但是即使可以
預見到所需的那些遍歷操作,你可能也不希望列表的介面中充斥著各種不同遍歷的操作。有
時還可能需要在同一個表列上同時進行多個遍歷。
迭代器模式都可幫你解決所有這些問題。這一模式的關鍵思想是將對列表的訪問和遍歷
從列表物件中分離出來並放入一個迭代器(i t e r a t o r)物件中。迭代器類定義了一個訪問該列
表元素的介面。迭代器物件負責跟蹤當前的元素; 即, 它知道哪些元素已經遍歷過了。
例如, 一個列表(L i s t)類可能需要一個列表迭代器(L i s t I t e r a t o r), 它們之間的關係如下圖:
在例項化列表迭代器之前,必須提供待遍歷的列表。一旦有了該列表迭代器的例項,就
可以順序地訪問該列表的各個元素。C u r r e n t I t e m操作返回表列中的當前元素, First操作初始化
迭代器,使當前元素指向列表的第一個元素, Next操作將當前元素指標向前推進一步,指向下
一個元素, 而I s D o n e檢查是否已越過最後一個元素,也就是完成了這次遍歷。
將遍歷機制與列表物件分離使我們可以定義不同的迭代器來實現不同的遍歷策略,而無
需在列表介面中列舉它們。例如, 過濾表列迭代器( F i l t e r i n g L i s t I t e r a t o r )可能只訪問那些滿足特
定過濾約束條件的元素。
注意迭代器和列表是耦合在一起的,而且客戶物件必須知道遍歷的是一個列表而不是其
他聚合結構。最好能有一種辦法使得不需改變客戶程式碼即可改變該聚合類。可以通過將迭代
器的概念推廣到多型迭代(polymorphic iteration)來達到這個目標。
例如, 假定我們還有一個列表的特殊實現,比如說S k i p L i s t [ P u g 9 0 ]。S k i p L i s t是一種具有
類似於平衡樹性質的隨機資料結構。我們希望我們的程式碼對L i s t和S k i p L i s t物件都適用。
首先,定義一個抽象列表類A b s t r a c t L i s t,它提供操作列表的公共介面。類似地,我們也
需要一個抽象的迭代器類I t e r a t o r,它定義公共的迭代介面。然後我們可以為每個不同的列表
實現定義具體的I t e r a t o r子類。這樣迭代機制就與具體的聚合類無關了。
餘下的問題是如何建立迭代器。既然要使這些程式碼不依賴於具體的列表子類, 就不能僅僅
簡單地例項化一個特定的類, 而要讓列表物件負責建立相應的迭代器。這需要列表物件提供
C r e a t e I t e r a t o r這樣的操作, 客戶請求呼叫該操作以獲得一個迭代器物件。
建立迭代器是一個Factory Method模式( 3 . 3)的例子。我們在這裡用它來使得一個客戶
可向一個列表物件請求合適的迭代器。Factory Method模式產生兩個類層次, 一個是列表的, 一
個是迭代器的。CreateIterator “聯絡” 這兩個類層次。
4. 適用性
迭代器模式可用來:
• 訪問一個聚合物件的內容而無需暴露它的內部表示。
• 支援對聚合物件的多種遍歷。
• 為遍歷不同的聚合結構提供一個統一的介面(即, 支援多型迭代)。
5. 結構
6. 參與者
• I t e r a t o r(迭代器)
- 迭代器定義訪問和遍歷元素的介面。
• C o n c r e t e I t e r a t o r(具體迭代器)
- 具體迭代器實現迭代器介面。
- 對該聚合遍歷時跟蹤當前位置。
• A g g r e g a t e(聚合)
- 聚合定義建立相應迭代器物件的介面。
• C o n c r e t e A g g r e g a t e(具體聚合)
- 具體聚合實現建立相應迭代器的介面,該操作返回C o n c r e t e I t e r a t o r的一個適當的例項。
7. 協作
• ConcreteIterator跟蹤聚合中的當前物件,並能夠計算出待遍歷的後繼物件。
8. 效果
迭代器模式有三個重要的作用:
1 ) 它支援以不同的方式遍歷一個聚合複雜的聚合可用多種方式進行遍歷。例如, 程式碼生
成和語義檢查要遍歷語法分析樹。程式碼生成可以按中序或者按前序來遍歷語法分析樹。迭代
器模式使得改變遍歷演算法變得很容易: 僅需用一個不同的迭代器的例項代替原先的例項即可。
你也可以自己定義迭代器的子類以支援新的遍歷。
2) 迭代器簡化了聚合的介面有了迭代器的遍歷介面,聚合本身就不再需要類似的遍歷
介面了。這樣就簡化了聚合的介面。
3) 在同一個聚合上可以有多個遍歷每個迭代器保持它自己的遍歷狀態。因此你可以同
時進行多個遍歷。
9. 實現
迭代器在實現上有許多變化和選擇。下面是一些較重要的實現。實現迭代器模式時常常
需要根據所使用的語言提供的控制結構來進行權衡。一些語言(例如, CLU[LG86])甚至直接支
持這一模式。
1) 誰控制該迭代一個基本的問題是決定由哪一方來控制該迭代, 是迭代器還是使用該迭
代器的客戶。當由客戶來控制迭代時, 該迭代器稱為一個外部迭代器(external iterator),而當
由迭代器控制迭代時, 該迭代器稱為一個內部迭代器(internal iterator) 。使用外部迭代器的客
戶必須主動推進遍歷的步伐,顯式地向迭代器請求下一個元素。相反地, 若使用內部迭代器,
客戶只需向其提交一個待執行的操作,而迭代器將對聚合中的每一個元素實施該操作。
外部迭代器比內部迭代器更靈活。例如, 若要比較兩個集合是否相等,這個功能很容易用
外部迭代器實現,而幾乎無法用內部迭代器實現。在象C + +這樣不提供匿名函式、閉包, 或象
S m a l l t a l k和CLOS 這樣不提供連續( c o n t i n u a t i o n )的語言中,內部迭代器的弱點更為明顯。但另
一方面, 內部迭代器的使用較為容易, 因為它們已經定義好了迭代邏輯。
2) 誰定義遍歷演算法迭代器不是唯一可定義遍歷演算法的地方。聚合本身也可以定義遍歷
演算法,並在遍歷過程中用迭代器來儲存當前迭代的狀態。我們稱這種迭代器為一個遊標
(cursor), 因為它僅用來指示當前位置。客戶會以這個遊標為一個引數呼叫該聚合的N e x t操作,
而N e x t操作將改變這個指示器的狀態。
如果迭代器負責遍歷演算法, 那麼將易於在相同的聚合上使用不同的迭代演算法, 同時也易於
在不同的聚合上重用相同的演算法。從另一方面說, 遍歷演算法可能需要訪問聚合的私有變數。如
果這樣,將遍歷演算法放入迭代器中會破壞聚合的封裝性。
3) 迭代器健壯程度如何在遍歷一個聚合的同時更改這個聚合可能是危險的。如果在遍
B o o c h分別稱外部和內部迭代器為主動( a c t i v e )和被動( p a s s i v e )迭代器[ B o o 9 4 ]。“主動”和“被動”兩個詞
描述了客戶的作用, 而不是指迭代器主動與否。
指示器是M e m e n t o模式的一個簡單例子並且有許多和它相同的實現問題。
歷聚合的時候增加或刪除該聚合元素, 可能會導致兩次訪問同一個元素或者遺漏掉某個元素。
一個簡單的解決辦法是拷貝該聚合,並對該拷貝實施遍歷, 但一般來說這樣做代價太高。
一個健壯的迭代器(robust iterator)保證插入和刪除操作不會干擾遍歷, 且不需拷貝該聚合。
有許多方法來實現健壯的迭代器。其中大多數需要向這個聚合註冊該迭代器。當插入或刪除
元素時,該聚合要麼調整迭代器的內部狀態, 要麼在內部的維護額外的資訊以保證正確的遍
歷。
K o f l e r在E T + + [ K o f 9 3 ]中對如何實現健壯的迭代器做了很充分的討論。M u r r a y討論瞭如何
為USL StandardComponents 列表類實現健壯的迭代器[ M u r 9 3 ]。
4) 附加的迭代器操作迭代器的最小介面由F i r s t、N e x t、I s D o n e和C u r r e n t I t e m 操作組成。
其他一些操作可能也很有用。例如, 對有序的聚合可用一個P r e v i o u s操作將迭代器定位到前一
個元素。S k i p To操作用於已排序並做了索引的聚合中,它將迭代器定位到符合指定條件的元
素物件上。
5) 在C + +中使用多型的迭代器使用多型迭代器是有代價的。它們要求用一個F a c t o r y
M e t h o d動態的分配迭代器物件。因此僅當必須多型時才使用它們。否則使用在棧中分配記憶體
的具體的迭代器。
多型迭代器有另一個缺點: 客戶必須負責刪除它們。這容易導致錯誤, 因為你容易忘記釋
放一個使用堆分配的迭代器物件,當一個操作有多個出口時尤其如此。而且其間如果有異常
被觸發的話,迭代器物件將永遠不會被釋放。
P r o x y(4 . 4)模式提供了一個補救方法。我們可使用一個棧分配的P r o x y作為實際迭代器
的中間代理。該代理在其析構器中刪除該迭代器。這樣當該代理生命週期結束時,實際迭代
器將同它一起被釋放。即使是在發生異常時,該代理機制能保證正確地清除迭代器物件。這
就是著名的C + +“資源分配即初始化”技術[ E S 9 0 ]的一個應用。下面的程式碼示例給出了一個例
子。
6) 迭代器可有特權訪問迭代器可被看為建立它的聚合的一個擴充套件。迭代器和聚合緊密
耦合。在C + +中我們可讓迭代器作為它的聚合的一個友元( f r i e n d )來表示這種緊密的關係。這
樣你就不需要在聚合類中定義一些僅為迭代器所使用的操作。
但是, 這樣的特權訪問可能使定義新的遍歷變得很難, 因為它將要求改變該聚合的介面增
加另一個友元。為避免這一問題, 迭代器類可包含一些p r o t e c t e d操作來訪問聚合類的重要的非
公共可見的成員。迭代器子類(且只有迭代器子類)可使用這些p r o t e c t e d操作來得到對該聚合的
特權訪問。
7) 用於複合物件的迭代器在C o m p o s i t e ( 4 . 3 )模式中的那些遞迴聚合結構上, 外部迭代器
可能難以實現, 因為在該結構中不同物件處於巢狀聚合的多個不同層次, 因此一個外部迭代
器為跟蹤當前的物件必須儲存一條縱貫該C o m p o s i t e的路徑。有時使用一個內部迭代器會更容
易一些。它僅需遞迴地呼叫自己即可,這樣就隱式地將路徑儲存在呼叫棧中,而無需顯式地
維護當前物件位置。
如果複合中的節點有一個介面可以從一個節點移到它的兄弟節點、父節點和子節點, 那麼
基於遊標的迭代器是個更好的選擇。遊標只需跟蹤當前的節點; 它可依賴這種節點介面來遍歷
甚至可以將N e x t,I s D o n e和C u r r e n t I t e m併入到一個操作中,該操作前進到下一個物件並返回這個物件,如果
遍歷結束,那麼這個操作返回一個特定的值(例如,0 )標誌該迭代結束。這樣我們就使這個介面變得更小了。
該複合物件。
複合常常需要用多種方法遍歷。前序, 後序, 中序以及廣度優先遍歷都是常用的。你可用
不同的迭代器類來支援不同的遍歷。
8) 空迭代器一個空迭代器( N u l l I t e r a t o r )是一個退化的迭代器, 它有助於處理邊界條件。
根據定義,一個N u l l I t e r a t o r總是已經完成了遍歷:即, 它的I s D o n e操作總是返回t r u e。
空迭代器使得更容易遍歷樹形結構的聚合(如複合物件)。在遍歷過程中的每一節點, 都可
向當前的元素請求遍歷其各個子結點的迭代器。該聚合元素將返回一個具體的迭代器。但葉
節點元素返回N u l l I t e r a t o r的一個例項。這就使我們可以用一種統一的方式實現在整個結構上
的遍歷。
10. 程式碼示例
我們將看看一個簡單L i s t類的實現, 它是我們的基礎庫(附錄C )的一部分。我們將給出兩個
迭代器的實現, 一個以從前到後的次序遍歷該表列, 而另一個以從後到前的次序遍歷(基礎庫只
支援第一種)。然後我們說明如何使用這些迭代器,以及如何避免限定於一種特定的實現。在
此之後, 我們將改變原來的設計以保證迭代器被正確的刪除。最後一個例子示例一個內部迭代
器並與其相應的外部迭代器進行比較。
1) 列表和迭代器介面首先讓我們看與實現迭代器相關的部分L i s t介面。完整的介面請參
考附錄C。
該L i s t類通過它的公共介面提供了一個合理的有效的途徑以支援迭代。它足以實現這兩種
遍歷。因此沒有必再要給迭代器對底層資料結構的訪問特權,也就是說,迭代器類不是列表
的友元。為確保對不同遍歷的透明使用, 我們定義一個抽象的迭代器類, 它定義了迭代器介面。
2) 迭代器子類的實現列表迭代器是迭代器的一個子類。
L i s t I t e r a t o r的實現簡單直接。它儲存L i s t和列表當前位置的索引_ c u r r e n t。
F i r s t將迭代器置於第一個元素:
N e x t使當前元素向前推進一步:
I s D o n e檢查指向當前元素的索引是否超出了列表:
最後, CurrentItem 返回當前索引指向的元素。若迭代已經終止, 則丟擲一個I t e r a t o r
O u t O f B o u n d s異常:
R e v e r s e L i s t I t e r a t o r的實現是幾乎是一樣的,只不過它的F i r s t操作將_ c u r r e n t置於列表的末
尾, 而N