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

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


第4章結構型模式
結構型模式涉及到如何組合類和物件以獲得更大的結構。結構型類模式採用繼承機制來
組合介面或實現。一個簡單的例子是採用多重繼承方法將兩個以上的類組合成一個類,結果
這個類包含了所有父類的性質。這一模式尤其有助於多個獨立開發的類庫協同工作。另外一
個例子是類形式的A d a p t e r ( 4 . 1 )模式。一般來說,介面卡使得一個介面( a d a p t e e的介面)與其他
介面相容,從而給出了多個不同介面的統一抽象。為此,類介面卡對一個a d a p t e e類進行私有
繼承。這樣,介面卡就可以用a d a p t e e的介面表示它的介面。
結構型物件模式不是對介面和實現進行組合,而是描述瞭如何對一些物件進行組合,從
而實現新功能的一些方法。因為可以在執行時刻改變物件組合關係,所以物件組合方式具有
更大的靈活性,而這種機制用靜態類組合是不可能實現的。
Composite (4.3) 模式是結構型物件模式的一個例項。它描述瞭如何構造一個類層次式結
構,這一結構由兩種型別的物件(基元物件和組合物件)所對應的類構成. 其中的組合物件使
得你可以組合基元物件以及其他的組合物件,從而形成任意複雜的結構。在Proxy (4.7) 模式
中,p r o x y物件作為其他物件的一個方便的替代或佔位符。它的使用可以有多種形式。例如它
可以在區域性空間中代表一個遠端地址空間中的物件,也可以表示一個要求被載入的較大的對
象,還可以用來保護對敏感物件的訪問。P r o x y模式還提供了對物件的一些特有性質的一定程
度上的間接訪問,從而它可以限制、增強或修改這些性質。
F l y w e i g h t ( 4 . 6 )模式為了共享物件定義了一個結構。至少有兩個原因要求物件共享:效率
和一致性。F l y w e i g h t的物件共享機制主要強調物件的空間效率。使用很多物件的應用必需考
慮每一個物件的開銷。使用物件共享而不是進行物件複製,可以節省大量的空間資源。但是
僅當這些物件沒有定義與上下文相關的狀態時,它們才可以被共享。F l y w e i g h t的物件沒有這
樣的狀態。任何執行任務時需要的其他一些資訊僅當需要時才傳遞過去。由於不存在與上下
文相關的狀態,因此F l y w e i g h t物件可以被自由地共享。
如果說F l y w e i g h t模式說明了如何生成很多較小的物件,那麼F a c a d e ( 4 . 5 )模式則描述瞭如
何用單個物件表示整個子系統。模式中的f a c a d e用來表示一組物件, f a c a d e的職責是將訊息轉
發給它所表示的物件。B r i d g e ( 4 . 2 )模式將物件的抽象和其實現分離,從而可以獨立地改變它
們。
D e c o r a t o r ( 4 . 4 )模式描述瞭如何動態地為物件新增職責。D e c o r a t o r模式是一種結構型模式。
這一模式採用遞迴方式組合物件,從而允許你新增任意多的物件職責。例如,一個包含使用者
介面元件的D e c o r a t o r物件可以將邊框或陰影這樣的裝飾新增到該元件中,或者它可以將視窗
滾動和縮放這樣的功能新增的元件中。我們可以將一個D e c o r a t o r物件巢狀在另外一個物件中
就可以很簡單地增加兩個裝飾,新增其他的裝飾也是如此。因此,每個D e c o r a t o r物件必須與
其元件的介面相容並且保證將訊息傳遞給它。D e c o r a t o r模式在轉發一條資訊之前或之後都可
以完成它的工作(比如繪製元件的邊框)。
許多結構型模式在某種程度上具有相關性,我們將在本章末討論這些關係。
4.1 ADAPTER(介面卡)—類物件結構型模式
1. 意圖
將一個類的介面轉換成客戶希望的另外一個介面。A d a p t e r模式使得原本由於介面不相容
而不能一起工作的那些類可以一起工作。
2. 別名
包裝器Wr a p p e r。
3. 動機
有時,為複用而設計的工具箱類不能夠被複用的原因僅僅是因為它的介面與專業應用領
域所需要的介面不匹配。
例如,有一個繪圖編輯器,這個編輯器允許使用者繪製和排列基本圖元(線、多邊型和正
文等)生成圖片和圖表。這個繪圖編輯器的關鍵抽象是圖形物件。圖形物件有一個可編輯的
形狀,並可以繪製自身。圖形物件的介面由一個稱為S h a p e的抽象類定義。繪圖編輯器為每一
種圖形物件定義了一個S h a p e的子類: L i n e S h a p e類對應於直線, P o l y g o n S h a p e類對應於多邊
型,等等。
像L i n e S h a p e和P o l y g o n S h a p e這樣的基本幾何圖形的類比較容易實現,這是由於它們的繪
圖和編輯功能本來就很有限。但是對於可以顯示和編輯正文的Te x t S h a p e子類來說,實現相當
困難,因為即使是基本的正文編輯也要涉及到複雜的螢幕重新整理和緩衝區管理。同時,成品的
使用者介面工具箱可能已經提供了一個複雜的Te x t Vi e w類用於顯示和編輯正文。理想的情況是
我們可以複用這個Te x t Vi e w類以實現Te x t S h a p e類,但是工具箱的設計者當時並沒有考慮
S h a p e的存在,因此Te x t Vi e w和S h a p e物件不能互換。
一個應用可能會有一些類具有不同的介面並且這些介面互不相容,在這樣的應用中象
Te x t Vi e w這樣已經存在並且不相關的類如何協同工作呢?我們可以改變Te x t Vi e w類使它相容
S h a p e類的介面,但前提是必須有這個工具箱的原始碼。然而即使我們得到了這些原始碼,修
改Te x t Vi e w也是沒有什麼意義的;因為不應該僅僅為了實現一個應用,工具箱就不得不採用
一些與特定領域相關的介面。
我們可以不用上面的方法,而定義一個Te x t S h a p e類,由它來適配Te x t Vi e w的介面和S h a p e
的介面。我們可以用兩種方法做這件事: 1) 繼承S h a p e類的介面和Te x t Vi e w的實現,或2) 將一
個Te x t Vi e w例項作為Te x t S h a p e的組成部分,並且使用Te x t Vi e w的介面實現Te x t S h a p e。這兩種
方法恰恰對應於A d a p t e r模式的類和物件版本。我們將Te x t S h a p e稱之為介面卡A d a p t e r。
9 2 設計模式:可複用面向物件軟體的基礎

上面的類圖說明了物件介面卡例項。它說明了在S h a p e類中宣告的B o u n d i n g B o x請求如何
被轉換成在Te x t Vi e w類中定義的G e t E x t e n t請求。由於Te x t S h a p e將Te x t Vi e w的介面與S h a p e的
介面進行了匹配,因此繪圖編輯器就可以複用原先並不相容的Te x t Vi e w類。
A d a p t e r時常還要負責提供那些被匹配的類所沒有提供的功能,上面的類圖中說明了適配
器如何實現這些職責。由於繪圖編輯器允許使用者互動的將每一個S h a p e物件“拖動”到一個新
的位置,而Te x t Vi e w設計中沒有這種功能。我們可以實現Te x t S h a p 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子類的一個例項。
M a n i p u l a t o r是一個抽象類,它所描述的物件知道如何驅動S h a p e類響應相應的使用者輸入,
例如將圖形拖動到一個新的位置。對應於不同形狀的圖形, M a n i p u l a t o r有不同的子類;例如
子類Te x t M a n i p u l a t o r對應於Te x t S h a p e。Te x t S h a p e通過返回一個Te x t M a n i p u l a t o r例項,增加了
Te x t Vi e w中缺少而S h a p e需要的功能。
4. 適用性
以下情況使用A d a p t e r模式
• 你想使用一個已經存在的類,而它的介面不符合你的需求。
• 你想建立一個可以複用的類,該類可以與其他不相關的類或不可預見的類(即那些介面
可能不一定相容的類)協同工作。
• (僅適用於物件A d a p t e r)你想使用一些已經存在的子類,但是不可能對每一個都進行
子類化以匹配它們的介面。物件介面卡可以適配它的父類介面。
5. 結構
類介面卡使用多重繼承對一個介面與另一個介面進行匹配,如下圖所示。
物件匹配器依賴於物件組合,如下圖所示。
6. 參與者
• Ta r g e t ( S h a p e )
— 定義C l i e n t使用的與特定領域相關的介面。
• C l i e n t ( D r a w i n g E d i t o r )
第4章結構型模式9 3

— 與符合Ta rg e t介面的物件協同。
• A d a p t e e ( Te x t Vi e w )
— 定義一個已經存在的介面,這個介面需要適配。
• A d a p t e r ( Te x t S h a p e )
— 對A d a p t e e的介面與Ta rg e t介面進行適配
7. 協作
• Client在A d a p t e r例項上呼叫一些操作。接著介面卡呼叫A d a p t e e的操作實現這個請求。
8. 效果
類介面卡和物件介面卡有不同的權衡。類介面卡
• 用一個具體的A d a p t e r類對A d a p t e e和Ta rg e t進行匹配。結果是當我們想要匹配一個類以
及所有它的子類時,類A d a p t e r將不能勝任工作。
• 使得A d a p t e r可以重定義A d a p t e e的部分行為,因為A d a p t e r是A d a p t e e的一個子類。
• 僅僅引入了一個物件,並不需要額外的指標以間接得到a d a p t e e。
物件介面卡則
• 允許一個A d a p t e r與多個A d a p t e e—即A d a p t e e本身以及它的所有子類(如果有子類的話)
—同時工作。A d a p t e r也可以一次給所有的A d a p t e e新增功能。
• 使得重定義A d a p t e e的行為比較困難。這就需要生成A d a p t e e的子類並且使得A d a p t e r引用
這個子類而不是引用A d a p t e e本身。
使用A d a p t e r模式時需要考慮的其他一些因素有:
1) Adapter的匹配程度對A d a p t e e的介面與Ta rg e t的介面進行匹配的工作量各個A d a p t e r可
能不一樣。工作範圍可能是,從簡單的介面轉換(例如改變操作名)到支援完全不同的操作集
合。A d a p t e r的工作量取決於Ta rg e t介面與A d a p t e e介面的相似程度。
2) 可插入的Adapter 當其他的類使用一個類時,如果所需的假定條件越少,這個類就更
具可複用性。如果將介面匹配構建為一個類,就不需要假定對其他的類可見的是一個相同的
介面。也就是說,介面匹配使得我們可以將自己的類加入到一些現有的系統中去,而這些系
統對這個類的介面可能會有所不同。O b j e c t - Wo r k / S m a l l t a l k [ P a r 9 0 ]使用pluggable adapter一詞
描述那些具有內部介面適配的類。
考慮Tr e e D i s p l a y視窗元件,它可以圖形化顯示樹狀結構。如果這是一個具有特殊用途的
視窗元件,僅在一個應用中使用,我們可能要求它所顯示的物件有一個特殊的介面,即它們
都是抽象類Tr e e的子類。如果我們希望使Tr e e D i s p l a y有具有良好的複用性的話(比如說,我
們希望將它作為可用視窗元件工具箱的一部分),那麼這種要求將是不合理的。應用程式將自
己定義樹結構類,而不應一定要使用我們的抽象類Tr e e。不同的樹結構會有不同的介面。
例如,在一個目錄層次結構中,可以通過G e t S u b d i r e c t o r i e s操作進行訪問子目錄,然而在
一個繼承式層次結構中,相應的操作可能被稱為G e t S u b c l a s s e s。儘管這兩種層次結構使用的
介面不同,一個可複用的Tr e e D i s p l a y視窗元件必須能顯示所有這兩種結構。也就是說,
Tr e e D i s p l a y應具有介面適配的功能。
我們將在實現一節討論在類中構建介面適配的多種方法。
3) 使用雙向介面卡提供透明操作使用介面卡的一個潛在問題是,它們不對所有的客戶
都透明。被適配的物件不再相容A d a p t e e的介面,因此並不是所有A d a p t e e物件可以被使用的
9 4 設計模式:可複用面向物件軟體的基礎

地方它都可以被使用。雙向介面卡提供了這樣的透明性。在兩個不同的客戶需要用不同的方
式檢視同一個物件時,雙向介面卡尤其有用。
考慮一個雙向介面卡,它將圖形編輯框架Unidraw [VL90] 與約束求解工具箱Q O C A
[ H H M V 9 2 ]整合起來。這兩個系統都有一些類,這些類顯式地表示變數: U n i d r a w含有類
S t a t e Va r i a b l e,Q O C A中含有類C o n s t r a i n t Va r i a b l e,如下圖所示。為了使U n i d r a w與Q O C A協同
工作,必須首先使類C o n s t r a i n t Va r i a b l e與類S t a t e Va r i a b l e相匹配;而為了將Q O C A的求解結果
傳遞給U n i d r a w,必須使S t a t e Va r i a b l e與C o n s t r a i n t Va r i a b l e相匹配。
這一方案中包含了一個雙向介面卡C o n s t r a i n t S t a t e Va r i a b l e,它是類C o n s t r a i n t Va r i a b l e與類
S t a t e Va r i a b l e共同的子類, C o n s t r a i n t S t a t e Va r i a b l e使得兩個介面互相匹配。在該例中多重繼承
是一個可行的解決方案,因為被適配類的介面差異較大。雙向介面卡與這兩個被匹配的類都
相容,在這兩個系統中它都可以工作。
9. 實現
儘管A d a p t e r模式的實現方式通常簡單直接,但是仍需要注意以下一些問題:
1) 使用C + +實現介面卡類在使用C + +實現介面卡類時, A d a p t e r類應該採用公共方式繼
承Ta rg e t類,並且用私有方式繼承A d a p t e e類。因此, A d a p t e r類應該是Ta rg e t的子型別,但不
是A d a p t e e的子型別。
2) 可插入的介面卡有許多方法可以實現可插入的介面卡。例如,前面描述的Tr e e D i s p l a y
視窗元件可以自動的佈置和顯示層次式結構,對於它有三種實現方法:
首先(這也是所有這三種實現都要做的)是為Adaptee 找到一個“窄”介面,即可用於適
配的最小操作集。因為包含較少操作的窄介面相對包含較多操作的寬介面比較容易進行匹配。
對於Tr e e D i s p l a y而言,被匹配的物件可以是任何一個層次式結構。因此最小介面集合僅包含
兩個操作:一個操作定義如何在層次結構中表示一個節點,另一個操作返回該節點的子節點。
對這個窄介面,有以下三個實現途徑:
a) 使用抽象操作在Tr e e D i s p l a y類中定義窄A d a p t e e介面相應的抽象操作。這樣就由子類
來實現這些抽象操作並匹配具體的樹結構的物件。例如, D i r e c t o r y Tr e e D i s p l a y子類將通過訪
問目錄結構實現這些操作,如下圖所示。
第4章結構型模式9 5

(到QOCA類層次結構) (到Unidraw 類層次結構)
D i r e c t o r y Tr e e D i s p l a y對這個窄介面加以特化,使得它的D i r e c t o r y B r o w s e r客戶可以用它來
顯示目錄結構。
b) 使用代理物件在這種方法中, Tr e e D i s p l a y將訪問樹結構的請求轉發到代理物件。
Tr e e D i s p l a y的客戶進行一些選擇,並將這些選擇提供給代理物件,這樣客戶就可以對適配加
以控制,如下圖所示。
例如,有一個D i r e c t o r y B r o w s e r,它像前面一樣使用Tr e e D i s p l a y。D i r e c t o r y B r o w s e r可能
為匹配Tr e e D i s p l a y和層次目錄結構構造出一個較好的代理。在S m a l l t a l k或Objective C這樣的
動態型別語言中,該方法只需要一個介面對介面卡註冊代理即可。然後Tr e e D i s p l a y簡單地將
請求轉發給代理物件。N E X T S T E P [ A d d 9 4 ]大量使用這種方法以減少子類化。
在C + +這樣的靜態型別語言中,需要一個代理的顯式介面定義。我們將Tr e e D i s p l a y需要
的窄介面放入純虛類Tr e e A c c e s s o r D e l e g a t e中,從而指定這樣的一個介面。然後我們可以運用
繼承機制將這個介面融合到我們所選擇的代理中—這裡我們選擇D i r e c t o r y B r o w s e r。如果
D i r e c t o r y B r o w s e r沒有父類我們將採用單繼承,否則採用多繼承。這種將類融合在一起的方法
相對於引入一個新的Tr e e D i s p l a y子類並單獨實現它的操作的方法要容易一些。
c) 引數化的介面卡通常在S m a l l t a l k中支援可插入介面卡的方法是,用一個或多個模組
對介面卡進行引數化。模組構造支援無子類化的適配。一個模組可以匹配一個請求,並且適
配器可以為每個請求儲存一個模組。在本例中意味著, Tr e e D i s p l a y儲存的一個模組用來將一
個節點轉化成為一個G r a p h i c N o d e,另外一個模組用來存取一個節點的子節點。
例如,當對一個目錄層次建立Tr e e D i s p l a y時,我們可以這樣寫:
如果你在一個類中建立介面適配,這種方法提供了另外一種選擇,它相對於子類化方法
來說更方便一些。
10. 程式碼示例
對動機一節中例子,從類S h a p e和Te x t Vi e w開始,我們將給出類介面卡和物件介面卡實現
程式碼的簡要框架。
9 6 設計模式:可複用面向物件軟體的基礎

S h a p e假定有一個邊框,這個邊框由它相對的兩角定義。而Te x t Vi e w則由原點、寬度和高
度定義。S h a p 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物件。當用戶操作
一個圖形時, M a n i p u l a t o r物件知道如何驅動這個圖形。Te x t Vi e w沒有等同的操作。
Te x t S h a p e類是這些不同介面間的介面卡。
類介面卡採用多重繼承適配介面。類介面卡的關鍵是用一個分支繼承介面,而用另外一
個分支繼承介面的實現部分。通常C + +中作出這一區分的方法是:用公共方式繼承介面;用
私有方式繼承介面的實現。下面我們按照這種常規方法定義Te x t S h a p e介面卡。
B o u n d i n g B o x操作對Te x t Vi e w的介面進行轉換使之匹配S h a p e的介面。
I s E m p t y操作給出了在介面卡實現過程中常用的一種方法:直接轉發請求:
最後,我們定義C r e a t e M a n i p u l a t o r(Te x t Vi e w不支援該操作),假定我們已經實現了支援
Te x t S h a p e操作的類Te x t M a n i p u l a t o r。
第4章結構型模式9 7

C r e a t e M a n i p u l a t o r是一個Factory Method的例項。
物件介面卡採用物件組合的方法將具有不同介面的類組合在一起。在該方法中,介面卡
Te x t S h a p e維護一個指向Te x t Vi e w的指標。
Te x t S h a p e必須在構造器中對指向Te x t Vi e w例項的指標進行初始化,當它自身的操作被調
用時,它還必須對它的Te x t Vi e w物件呼叫相應的操作。在本例中,假設客戶建立了Te x t Vi e w
物件並且將其傳遞給Te x t S h a p e的構造器:
C r e a t e M a n i p u l a t o r的實現程式碼與類介面卡版本的實現程式碼一樣,因為它的實現從零開始,
沒有複用任何Te x t Vi e w已有的函式。
將這段程式碼與類介面卡的相應程式碼進行比較,可以看出編寫物件介面卡程式碼相對麻煩一
些,但是它比較靈活。例如,客戶僅需將Te x t Vi e w子類的一個例項傳給Te x t S h a p e類的構造函
數,物件介面卡版本的Te x t S h a p e就同樣可以與Te x t Vi e w子類一起很好的工作。
11. 已知應用
意圖一節的例子來自一個基於E T + + [ W G M 8 8 ]的繪圖應用程式E T + + D r a w,E T + + D r a w通
過使用一個Te x t S h a p e介面卡類的方式複用了E T + +中一些類,並將它們用於正文編輯。
I n t e r Vi e w 2 . 6為諸如s c r o l l b a r s、b u t t o n s和m e n u s的使用者介面元素定義了一個抽象類
I n t e r a c t o r [ V L 8 8 ],它同時也為l i n e、c i r c l e、p o l y g o n和s p l i n e這樣的結構化圖形物件定義了一
個抽象類G r a p h i c s。I n t e r a c t o r和G r a p h i c s都有圖形外觀,但它們有著不同的介面和實現(它們
沒有同一個父類),因此它們並不相容。也就是說,你不能直接將一個結構化的圖形物件嵌入
9 8 設計模式:可複用面向物件軟體的基礎

一個對話方塊中。
而I n t e r Vi e w 2 . 6定義了一個稱為G r a p h i c B l o c k的物件介面卡,它是I n t e r a c t o r的子類,包含
G r a p h i c類的一個例項。G r a p h i c B l o c k將G r a p h i c類的介面與I n t e r a c t o r類的介面進行匹配。
G r a p h i c B l o c k使得一個G r a p h i c的例項可以在I n t e r a c t o r結構中被顯示、滾動和縮放。
可插入的介面卡在O b j e c t Wo r k s / S m a l l t a l k [ P a r 9 0 ]中很常見。標準S m a l l t a l k為顯示單個值的檢視
定義了一個Va l u e M o d e l類。為訪問這個值,Va l u e M o d e l定義了一個“v a l u e”和“v a l u e :”介面。這
些都是抽象方法。應用程式設計師用與特定領域相關的名字訪問這個值,如“w i d t h”和“w i d t h :”,但
為了使特定領域相關的名字與Va l u e M o d e l的介面相匹配,他們不一定要生成Va l u e M o d e l的子類。
而O b j e c t Wo r k s / S m a l l t a l k包含了一個Va l u e M o d e l類的子類,稱為P l u g g a b l e A d a p t o r。
P l u g g a b l e A d a p t o r物件可以將其他物件與Va l u e M o d e l的介面(“v a l u e”和“v a l u e :”)相匹配。它可
以用模組進行引數化,以便獲取和設定所期望的值。P l u g g a b l e A d a p t o r在其內部使用這些模組以
實現“v a l u e”和“v a l u e :”介面,如下圖所示。為語法上方便起見,P l u g g a b l e A d a p t o r也允許你直
接傳遞選擇器的名字(例如“w i d t h”和“w i d t h :”),它自動將這些選擇器轉換為相應的模組。
另外一個來自O b j e c t Wo r k s / S m a l l t a l k的例子是Ta b l e A d a p t o r類,它可以將一個物件序列與
一個表格表示相匹配。這個表格在每行顯示一個物件。客戶用表格可以使用的訊息集對
TableAdaptor 進行引數設定,從一個物件得到行屬性。
在N e X T的A p p K i t [ A d d 9 4 ]中,一些類使用代理物件進行介面匹配。一個例子是類
N X B r o w s e r,它可以顯示層次式資料列表。N X B r o w s e r類用一個代理物件存取並適配資料。
M a y e r的“Marriage of Convenience”[ M e y 8 8 ]是一種形式的類介面卡。M a y e r描述了
F i x e d S t a c k類如何匹配一個A r r a y類的實現部分和一個S t a c k類的介面部分。結果是一個包含一
定數目專案的棧。
12. 相關模式
模式B r i d g e ( 4 . 2 )的結構與物件介面卡類似,但是B r i d g e模式的出發點不同: B r i d g e目的是
將介面部分和實現部分分離,從而對它們可以較為容易也相對獨立的加以改變。而A d a p t e r則
意味著改變一個已有物件的介面。
D e c o r a t o r ( 4 . 4 )模式增強了其他物件的功能而同時又不改變它的介面。因此d e c o r a t o r對應
用程式的透明性比介面卡要好。結果是d e c o r a t o r支援遞迴組合,而純粹使用介面卡是不可能
實現這一點的。
模式P r o x y ( 4 . 7 )在不改變它的介面的條件下,為另一個物件定義了一個代理。
第4章結構型模式9 9

4.2 BRIDGE(橋接)—物件結構型模式
1. 意圖
將抽象部分與它的實現部分分離,使它們都可以獨立地變化。
2. 別名
H a n d l e / B o d y
3. 動機
當一個抽象可能有多個實現時,通常用繼承來協調它們。抽象類定義對該抽象的介面,
而具體的子類則用不同方式加以實現。但是此方法有時不夠靈活。繼承機制將抽象部分與它
的實現部分固定在一起,使得難以對抽象部分和實現部分獨立地進行修改、擴充和重用。
讓我們考慮在一個使用者介面工具箱中,一個可移植的Wi n d o w抽象部分的實現。例如,這
一抽象部分應該允許使用者開發一些在X Window System和I B M的Presentation Manager(PM)系
統中都可以使用的應用程式。運用繼承機制,我們可以定義Wi n d o w抽象類和它的兩個子類
XWi n d o w與P M Wi n d o w,由它們分別實現不同系統平臺上的Wi n d o w介面。但是繼承機制有兩
個不足之處:
1) 擴充套件Wi n d o w抽象使之適用於不同種類的視窗或新的系統平臺很不方便。假設有
Wi n d o w的一個子類I c o n Wi n d o w,它專門將Wi n d o w抽象用於圖示處理。為了使I c o n Wi n d o w支
持兩個系統平臺,我們必須實現兩個新類X I c o n Wi n d o w和P M I c o n Wi n d o w,更為糟糕的是,
我們不得不為每一種型別的視窗都定義兩個類。而為了支援第三個系統平臺我們還必須為每
一種視窗定義一個新的Wi n d o w子類,如下圖所示。
2) 繼承機制使得客戶程式碼與平臺相關。每當客戶建立一個視窗時,必須要例項化一個具
體的類,這個類有特定的實現部分。例如,建立X w i n d o w物件會將Wi n d o w抽象與X Wi n d o w
的實現部分繫結起來,這使得客戶程式依賴於X Wi n d o w的實現部分。這將使得很難將客戶代
碼移植到其他平臺上去。
客戶在建立視窗時應該不涉及到其具體實現部分。僅僅是視窗的實現部分依賴於應用運
行的平臺。這樣客戶程式碼在建立視窗時就不應涉及到特定的平臺。
B r i d g e模式解決以上問題的方法是,將Wi n d o w抽象和它的實現部分分別放在獨立的類層
次結構中。其中一個類層次結構針對視窗介面( Wi n d o w、I c o n Wi n d o w、Tr a n s i e n t Wi n d o w),
另外一個獨立的類層次結構針對平臺相關的視窗實現部分,這個類層次結構的根類為
Wi n d o w I m p。例如X w i n d o w I m p子類提供了一個基於X Wi n d o w系統的實現,如下頁上圖所示。
對Wi n d o w子類的所有操作都是用Wi n d o w I m p介面中的抽象操作實現的。這就將視窗的抽
象與系統平臺相關的實現部分分離開來。因此,我們將Wi n d o w與Wi n d o w I m p之間的關係稱之
為橋接,因為它在抽象類與它的實現之間起到了橋樑作用,使它們可以獨立地變化。
1 0 0 設計模式:可複用面向物件軟體的基礎

4. 適用性
以下一些情況使用B r i d g e模式:
• 你不希望在抽象和它的實現部分之間有一個固定的繫結關係。例如這種情況可能是因為,
在程式執行時刻實現部分應可以被選擇或者切換。
• 類的抽象以及它的實現都應該可以通過生成子類的方法加以擴充。這時B r i d g e模式使你
可以對不同的抽象介面和實現部分進行組合,並分別對它們進行擴充。
• 對一個抽象的實現部分的修改應對客戶不產生影響,即客戶的程式碼不必重新編譯。
• (C + +)你想對客戶完全隱藏抽象的實現部分。在C + +中,類的表示在類介面中是可見
的。
• 正如在意圖一節的第一個類圖中所示的那樣,有許多類要生成。這樣一種類層次結構說
明你必須將一個物件分解成兩個部分。R u m b a u g h稱這種類層次結構為“巢狀的普化”
(nested generalizations)。
• 你想在多個物件間共享實現(可能使用引用計數),但同時要求客戶並不知道這一點。
一個簡單的例子便是C o p l i e n的S t r i n g類[ C o p 9 2 ],在這個類中多個物件可以共享同一個字
符串表示( S t r i n g R e p)。
5. 結構
第4章結構型模式1 0 1

6. 參與者
• Abstraction (Wi n d o w )
— 定義抽象類的介面。
— 維護一個指向I m p l e m e n t o r型別物件的指標。
• RefinedAbstraction (IconWi n d o w )
— 擴充由A b s t r a c t i o n定義的介面。
• Implementor (Wi n d o w I m p )
— 定義實現類的介面,該介面不一定要與A b s t r a c t i o n的介面完全一致;事實上這兩個
介面可以完全不同。一般來講, I m p l e m e n t o r介面僅提供基本操作,而A b s t r a c t i o n則
定義了基於這些基本操作的較高層次的操作。
• ConcreteImplementor (XwindowImp, PMWi n d o w I m p )
— 實現I m p l e m e n t o r介面並定義它的具體實現。
7. 協作
• Abstraction將c l i e n t的請求轉發給它的I m p l e m e n t o r物件。
8. 效果
B r i d g e模式有以下一些優點:
1) 分離介面及其實現部分一個實現未必不變地繫結在一個介面上。抽象類的實現可以
在執行時刻進行配置,一個物件甚至可以在執行時刻改變它的實現。
將A b s t r a c t i o n與I m p l e m e n t o r分離有助於降低對實現部分編譯時刻的依賴性,當改變一個
實現類時,並不需要重新編譯A b s t r a c t i o n類和它的客戶程式。為了保證一個類庫的不同版本
之間的二進位制相容性,一定要有這個性質。
另外,介面與實現分離有助於分層,從而產生更好的結構化系統,系統的高層部分僅需
知道A b s t r a c t i o n和I m p l e m e n t o r即可。
2) 提高可擴充性你可以獨立地對A b s t r a c t i o n和I m p l e m e n t o r層次結構進行擴充。
3 ) 實現細節對客戶透明你可以對客戶隱藏實現細節,例如共享I m p l e m e n t o r物件以及相
應的引用計數機制(如果有的話)。
9. 實現
使用B r i d g e模式時需要注意以下一些問題:
1) 僅有一個Implementor 在僅有一個實現的時候,沒有必要建立一個抽象的I m p l e m e n t o r
類。這是B r i d g e模式的退化情況;在A b s t r a c t i o n與I m p l e m e n t o r之間有一種一對一的關係。盡
管如此,當你希望改變一個類的實現不會影響已有的客戶程式時,模式的分離機制還是非常
有用的—也就是說,不必重新編譯它們,僅需重新連線即可。
C a r o l a n [ C a r 8 9 ]用“常露齒嘻笑的貓”(Cheshire Cat)描述這一分離機制。在C + +中,
I m p l e m e n t o r類的類介面可以在一個私有的標頭檔案中定義,這個檔案不提供給客戶。這樣你就
對客戶徹底隱藏了一個類的實現部分。
2) 建立正確的I m p l e m e n t o r物件當存在多個I m p l e m e n t o r類的時候,你應該用何種方法,
在何時何處確定建立哪一個I m p l e m e n t o r類呢?
如果A b s t r a c t i o n知道所有的C o n c r e t e I m p l e m e n t o r類,它就可以在它的構造器中對其中的
一個類進行例項化,它可以通過傳遞給構造器的引數確定例項化哪一個類。例如,如果一個
1 0 2 設計模式:可複用面向物件軟體的基礎

c o l l e c t i o n類支援多重實現,就可以根據c o l l e c t i o n的大小決定例項化哪一個類。連結串列的實現可
以用於較小的c o l l e c t i o n類,而h a s h表則可用於較大的c o l l e c t i o n類。
另外一種方法是首先選擇一個預設的實現,然後根據需要改變這個實現。例如,如果一
個c o l l e c t i o n的大小超出了一定的閾值時,它將會切換它的實現,使之更適用於表目較多的
c o l l e c t i o n。
也可以代理給另一個物件,由它一次決定。在Wi n d o w / Wi n d o w I m p的例子中,我們可以
引入一個f a c t o r y物件(參見Abstract Factory(3.1)),該物件的唯一職責就是封裝系統平臺的細
節。這個物件知道應該為所用的平臺建立何種型別的Wi n d o w I m p物件;Wi n d o w僅需向它請求
一個Wi n d o w I m p,而它會返回正確型別的Wi n d o w I m p物件。這種方法的優點是Abstraction 類
不和任何一個I m p l e m e n t o r類直接耦合。
3 ) 共享I m p l e m e n t o r物件C o p l i e n闡明瞭如何用C + +中常用的H a n d l e / B o d y方法在多個物件
間共享一些實現[ C o p 9 2 ]。其中B o d y有一個物件引用計數器, H a n d l e對它進行增減操作。將共
享程式體賦給控制代碼的程式碼一般具有以下形式:
4) 採用多重繼承機制在C + +中可以使用多重繼承機制將抽象介面和它的實現部分結合起
來[ M a r 9 1 ] 。例如,一個類可以用p u b l i c 方式繼承A b s t r a c t i o n而以p r i v a t e 方式繼承
C o n c r e t e I m p l e m e n t o r。但是由於這種方法依賴於靜態繼承,它將實現部分與介面固定不變的
繫結在一起。因此不可能使用多重繼承的方法實現真正的B r i d g e模式—至少用C + +不行。
10. 程式碼示例
下面的C + +程式碼實現了意圖一節中Wi n d o w / Wi n d w o I m p的例子,其中Wi n d o w類為客戶應
用程式定義了視窗抽象類:
第4章結構型模式1 0 3

Wi n d o w維護一個對Wi n d o w I m p的引用,Wi n d o w I m p抽象類定義了一個對底層視窗系統的
介面。
Wi n d o w的子類定義了應用程式可能用到的不同型別的視窗,如應用視窗、圖示、對話方塊
臨時視窗以及工具箱的移動面板等等。
例如A p p l i c a t i o n Wi n d o w類將實現D r a w C o n t e n t s操作以繪製它所儲存的Vi e w例項:
I c o n Wi n d o w中儲存了它所顯示的圖示對應的點陣圖名. . .
. . .並且實現D r a w C o n t e n t s操作將這個點陣圖繪製在視窗上:
1 0 4 設計模式:可複用面向物件軟體的基礎

我們還可以定義許多其他型別的Window 類,例如Tr a n s i e n t Wi n d o w在與客戶對話時由一
個視窗建立,它可能要和這個建立它的視窗進行通訊; P a l e t t e Window 總是在其他視窗之上;
I c o n D o c k Wi n d o w擁有一些I c o n Wi n d o w,並且由它負責將它們排列整齊。
Wi n d o w的操作由Wi n d o w I m p的介面定義。例如,在呼叫Wi n d o w I m p操作在視窗中繪製矩
形之前,D r a w R e c t必須從它的兩個P o i n t引數中提取四個座標值:
具體的Wi n d o w I m p子類可支援不同的視窗系統, X w i n d o w I m p子類支援X Wi n d o w視窗系
統:
對於Presentation Manager (PM),我們定義P M Wi n d o w I m p類:
這些子類用視窗系統的基本操作實現Wi n d o w I m p操作,例如,對於X 視窗系統這樣實現
D e v i c e R e c t:
P M的實現部分可能象下面這樣:
第4章結構型模式1 0 5

那麼一個視窗怎樣得到正確的Wi n d o w I m p子類的例項呢?在本例我們假設Window 類具
有這個職責,它的G e t Wi n d o w I m p操作負責從一個抽象工廠(參見Abstract Factory(3.1)模式)
得到正確的例項,這個抽象工廠封裝了所有視窗系統的細節。
Wi n d o w S y s t e m F a c t o r y : : I n s t a n c e ( )函式返回一個抽象工廠,該工廠負責處理所有與特定窗
口系統相關的物件。為簡化起見,我們將它建立一個單件( S i n g l e t o n),允許Wi n d o w類直接
訪問這個工廠。
11. 已知應用
上面的Wi n d o w例項來自於E T + + [ W G M 8 8 ]。在E T + +中,Wi n d o w I m p稱為“Wi n d o w P o r t”,
它有X Wi n d o w P o r t和S u n Wi n d o w P o r t這樣一些子類。Wi n d o w物件請求一個稱為
“Wi n d o w S y s t e m”的抽象工廠建立相應的I m p l e m e n t o r物件。Wi n d o w S y s t e m提供了一個介面
用於建立一些與特定平臺相關的物件,例如字型、游標、點陣圖等。
E T + +的Wi n d o w / Wi n d o w P o r t設計擴充套件了B r i d g e模式,因為Wi n d o w P o r t保留了一個指回
Wi n d o w的指標。Wi n d o w P o r t的I m p l e m e n t o r類用這個指標通知Wi n d o w物件發生了一些與
Wi n d o w P o r t相關的事件:例如輸入事件的到來,視窗調整大小等。
Coplien[Cop92] 和S t r o u s t r u p [ S t r 9 1 ]都提及H a n d l e類並給出了一些例子。這些例子集中處
理一些記憶體管理問題,例如共享字串表示式以及支援大小可變的物件等。我們主要關心它
怎樣支援對一個抽象和它的實現進行獨立地擴充套件。
l i b g + + [ L e a 8 8 ]類庫定義了一些類用於實現公共的資料結構,例如S e t、L i n k e d S e t、
H a s h S e t、L i n k e d L i s t和H a s h Ta b l e。S e t是一個抽象類,它定義了一組抽象介面,而L i n k e d L i s t
和H a s h Ta b l e則分別是連結串列和h a s h表的具體實現。L i n k e d S e t和H a s h S e t是S e t的實現者,它們橋
接了S e t和它們具體所對應的L i n k e d L i s t和H a s h Table. 這是一種退化的橋接模式,因為沒有抽象
I m p l e m e n t o r類。
1 0 6 設計模式:可複用面向物件軟體的基礎

N e X T s AppKit[Add94]在圖象生成和顯示中使用了B r i d g e模式。一個圖象可以有多種不
同的表示方式,一個圖象的最佳顯示方式取決於顯示裝置的特性,特別是它的色彩數目和分
辨率。如果沒有A p p K i t的幫助,每一個應用程式中應用開發者都要確定在不同的情況下應該
使用哪一種實現方法。
為了減輕開發者的負擔, A p p K i t提供了N X I m a g e / N X I m a g e R e p橋接。N T I m a g e定義了圖
象處理的介面,而圖象介面的實現部分則定義在獨立的N X I m a g e R e p類層次中,這個類層次包
含了多個子類,如NXEPSImageRep, NXCachedImageRep和N X B i t M a p I m a g e R e p等。N X I m a g e
維護一個指標,指向一個或多個N X I m a g e R e p物件。如果有多個圖象實現, N X I m a g e會選擇一
個最適合當前顯示裝置的圖象實現。必要時N X I m a g e還可以將一個實現轉換成另一個實現。
這個B r i d g e模式變種很有趣的地方是: N X I m a g e能同時儲存多個N X I m a g e R e p實現。
12. 相關模式
Abstract Factory(3.1) 模式可以用來建立和配置一個特定的B r i d g e模式。
Adapter(4.1) 模式用來幫助無關的類協同工作,它通常在系統設計完成後才會被使用。然
而,B r i d g e模式則是在系統開始時就被使用,它使得抽象介面和實現部分可以獨立進行改變。
4.3 COMPOSITE(組合)—物件結構型模式
1. 意圖
將物件組合成樹形結構以表示“部分-整體”的層次結構。C o m p o s i t e使得使用者對單個物件
和組合物件的使用具有一致性。
2. 動機
在繪圖編輯器和圖形捕捉系統這樣的圖形應用程式中,使用者可以使用簡單的元件建立復
雜的圖表。使用者可以組合多個簡單元件以形成一些較大的元件,這些元件又可以組合成更大
的元件。一個簡單的實現方法是為Te x t和L i n e這樣的圖元定義一些類,另外定義一些類作為這
些圖元的容器類( C o n t a i n e r )。
然而這種方法存在一個問題:使用這些類的程式碼必須區別對待圖元物件與容器物件,而
實際上大多數情況下使用者認為它們是一樣的。對這些類區別使用,使得程式更加複雜。
C o m p o s i t e模式描述瞭如何使用遞迴組合,使得使用者不必對這些類進行區別,如下圖所示。
Composite 模式的關鍵是一個抽象類,它既可以代表圖元,又可以代表圖元的容器。在圖
形系統中的這個類就是G r a p h i c,它宣告一些與特定圖形物件相關的操作,例如D r a w。同時它
第4章結構型模式1 0 7

也聲明瞭所有的組合物件共享的一些操作,例如一些操作用於訪問和管理它的子部件。
子類L i n e、R e c t a n g l e和Te x t(參見前面的類圖)定義了一些圖元物件,這些類實現D r a w,
分別用於繪製直線、矩形和正文。由於圖元都沒有子圖形,因此它們都不執行與子類有關的
操作。
P i c t u r e類定義了一個Graphic 物件的聚合。Picture 的D r a w操作是通過對它的子部件呼叫
D r a w實現的, P i c t u r e還用這種方法實現了一些與其子部件相關的操作。由於P i c t u r e介面與
G r a p h i c介面是一致的,因此P i c t u r e物件可以遞迴地組合其他P i c t u r e物件。
下圖是一個典型的由遞迴組合的G r a p h i c物件組成的組合物件結構。
3. 適用性
以下情況使用C o m p o s i t e模式:
• 你想表示物件的部分-整體層次結構。
• 你希望使用者忽略組合物件與單個物件的不同,使用者將統一地使用組合結構中的所有對
象。
4. 結構
典型的C o m p o s i t e物件結構如下圖所示。
1 0 8 設計模式:可複用面向物件軟體的基礎

5. 參與者
• Component (Graphic)
— 為組合中的物件宣告介面。
— 在適當的情況下,實現所有類共有介面的預設行為。
— 宣告一個介面用於訪問和管理C o m p o n e n t的子元件。
—(可選)在遞迴結構中定義一個介面,用於訪問一個父部件,並在合適的情況下實現它。
• Leaf (Rectangle、L i n e、Te x t等)
— 在組合中表示葉節點物件,葉節點沒有子節點。
— 在組合中定義圖元物件的行為。
• Composite (Picture)
— 定義有子部件的那些部件的行為。
— 儲存子部件。
— 在C o m p o n e n t介面中實現與子部件有關的操作。
• Client
— 通過C o m p o n e n t介面操縱組合部件的物件。
6. 協作
• 使用者使用C o m p o n e n t類介面與組合結構中的物件進行互動。如果接收者是一個葉節點,則
直接處理請求。如果接收者是Composite, 它通常將請求傳送給它的子部件,在轉發請求
之前與/或之後可能執行一些輔助操作。
7. 效果
C o m p o s i t e模式
• 定義了包含基本物件和組合物件的類層次結構基本物件可以被組合成更復雜的組合對
象,而這個組合物件又可以被組合,這樣不斷的遞迴下去。客戶程式碼中,任何用到基本
物件的地方都可以使用組合物件。
• 簡化客戶程式碼客戶可以一致地使用組合結構和單個物件。通常使用者不知道(也不關心)
處理的是一個葉節點還是一個組合元件。這就簡化了客戶程式碼, 因為在定義組合的那些
類中不需要寫一些充斥著選擇語句的函式。
• 使得更容易增加新型別的元件新定義的C o m p o s i t e或L e a f子類自動地與已有的結構和客
戶程式碼一起工作,客戶程式不需因新的C o m p o n e n t類而改變。
• 使你的設計變得更加一般化容易增加新元件也會產生一些問題,那就是很難限制組合
中的元件。有時你希望一個組合只能有某些特定的元件。使用C o m p o s i t e時,你不能依
賴型別系統施加這些約束,而必須在執行時刻進行檢查。
8. 實現
我們在實現C o m p o s i t e模式時需要考慮以下幾個問題:
1 ) 顯式的父部件引用保持從子部件到父部件的引用能簡化組合結構的遍歷和管理。父部
件引用可以簡化結構的上移和元件的刪除,同時父部件引用也支援Chain of Responsibility(5.2)
模式。
通常在C o m p o n e n t類中定義父部件引用。L e a f和C o m p o s i t e類可以繼承這個引用以及管理
這個引用的那些操作。
第4章結構型模式1 0 9

對於父部件引用,必須維護一個不變式,即一個組合的所有子節點以這個組合為父節點,
而反之該組合以這些節點為子節點。保證這一點最容易的辦法是,僅當在一個組合中增加或
刪除一個元件時,才改變這個元件的父部件。如果能在C o m p o s i t e類的Add 和R e m o v e操作中
實現這種方法,那麼所有的子類都可以繼承這一方法,並且將自動維護這一不變式。
2 ) 共享元件共享元件是很有用的,比如它可以減少對存貯的需求。但是當一個元件只
有一個父部件時,很難共享元件。
一個可行的解決辦法是為子部件存貯多個父部件,但當一個請求在結構中向上傳遞時,
這種方法會導致多義性。F l y w e i g h t ( 4 . 6 )模式討論瞭如何修改設計以避免將父部件存貯在一起
的方法。如果子部件可以將一些狀態(或是所有的狀態)儲存在外部,從而不需要向父部件傳送
請求,那麼這種方法是可行的。
3) 最大化C o m p o n e n t介面C o m p o s i t e模式的目的之一是使得使用者不知道他們正在使用的
具體的Leaf 和C o m p o s i t e類。為了達到這一目的, C o m p o s i t e類應為Leaf 和C o m p o s i t e類儘可能
多定義一些公共操作。C o m p o s i t e類通常為這些操作提供預設的實現,而Leaf 和C o m p o s i t e子
類可以對它們進行重定義。
然而,這個目標有時可能會與類層次結構設計原則相沖突,該原則規定:一個類只能定
義那些對它的子類有意義的操作。有許多C o m p o n e n t所支援的操作對L e a f類似乎沒有什麼意義,
那麼C o m p o n e n t怎樣為它們提供一個預設的操作呢?
有時一點創造性可以使得一個看起來僅對C o m p o s i t e 才有意義的操作,將它移入
C o m p o n e n t類中,就會對所有的C o m p o n e n t都適用。例如,訪問子節點的介面是C o m p o s i t e類
的一個基本組成部分,但對L e a f類來說並不必要。但是如果我們把一個L e a f看成一個沒有子
節點的Component, 就可以為在C o m p o n e n t類中定義一個預設的操作,用於對子節點進行訪問,
這個預設的操作不返回任何一個子節點。Leaf 類可以使用預設的實現,而C o m p o s i t e類則會重
新實現這個操作以返回它們的子類。
管理子部件的操作比較複雜,我們將在下一項中予以討論。
4) 宣告管理子部件的操作雖然C o m p o s i t e類實現了Add 和R e m o v e操作用於管理子部件,
但在C o m p o s i t e模式中一個重要的問題是:在C o m p o s i t e類層次結構中哪一些類宣告這些操作。
我們是應該在C o m p o n e n t中宣告這些操作,並使這些操作對L e a f類有意義呢,還是隻應該在
C o m p o s i t e和它的子類中宣告並定義這些操作呢?
這需要在安全性和透明性之間做出權衡選擇。
• 在類層次結構的根部定義子節點管理介面的方法具有良好的透明性,因為你可以一致地
使用所有的元件,但是這一方法是以安全性為代價的,因為客戶有可能會做一些無意義
的事情,例如在Leaf 中增加和刪除物件等。
• 在C o m p o s i t e類中定義管理子部件的方法具有良好的安全性,因為在象C + +這樣的靜態
型別語言中,在編譯時任何從Leaf 中增加或刪除物件的嘗試都將被發現。但是這又損失
了透明性,因為Leaf 和C o m p o s i t e具有不同的介面。
在這一模式中,相對於安全性,我們比較強調透明性。如果你選擇了安全性,有時你可
能會丟失型別資訊,並且不得不將一個元件轉換成一個組合。這樣的型別轉換必定不是型別
安全的。
一種辦法是在C o m p o n e n t類中宣告一個操作Composite* GetComposite()。C o m p o n e n t提供
1 1 0 設計模式:可複用面向物件軟體的基礎

了一個返回空指標的預設操作。C o m p o s i t e類重新定義這個操作並通過t h i s指標返回它自身。
GetComposite 允許你查詢一個元件看它是否是一個組合,你可以對返回的組合安全地執
行Add 和R e m o v e操作。
你可使用C++ 中的d y n a m i c _ c a s t結構對C o m p o s i t e做相似的試驗。
當然,這裡的問題是我們對所有的元件的處理並不一致。在進行適當的動作之前,我們
必須檢測不同的型別。
提供透明性的唯一方法是在C o m p o n e n t中定義預設Add 和R e m o v e操作。這又帶來了一個
新的問題: C o m p o n e n t : : A d d 的實現不可避免地會有失敗的可能性。你可以不讓
C o m p o n e n t : : A d d做任何事情,但這就忽略了一個很重要的問題:企圖向葉節點中增加一些東
西時可能會引入錯誤。這時A d d操作會產生垃圾。你可以讓A d d操作刪除它的引數,但可能客
戶並不希望這樣。
如果該元件不允許有子部件,或者R e m o v e的引數不是該元件的子節點時,通常最好使用
預設方式(可能是產生一個異常)處理A d d和R e m o v e的失敗。
另一個辦法是對“刪除”的含義作一些改變。如果該元件有一個父部件引用,我們可重
新定義Component :: Remove,在它的父元件中刪除掉這個元件。然而,對應的A d d操作仍然沒
有合理的解釋。
5) Component是否應該實現一個C o m p o n e n t列表你可能希望在C o m p o n e n t類中將子節點
集合定義為一個例項變數,而這個C o m p o n e n t類中也聲明瞭一些操作對子節點進行訪問和管
第4章結構型模式1 1 1

理。但是在基類中存放子類指標,對葉節點來說會導致空間浪費,因為葉節點根本沒有子節
點。只有當該結構中子類數目相對較少時,才值得使用這種方法。
6) 子部件排序許多設計指定了C o m p o s i t e的子部件順序。在前面的G r a p h i c s例子中,排
序可能表示了從前至後的順序。如果C o m p o s i t e表示語法分析樹, C o m p o s i t e子部件的順序必
須反映程式結構,而組合語句就是這樣一些C o m p o s i t e的例項。
如果需要考慮子節點的順序時,必須仔細地設計對子節點的訪問和管理介面,以便管理
子節點序列。I t e r a t o r模式( 5 . 4 )可以在這方面給予一些定的指導。
7) 使用高速緩衝存貯改善效能如果你需要對組合進行頻繁的遍歷或查詢, C o m p o s i t e類
可以緩衝儲存對它的子節點進行遍歷或查詢的相關資訊。C o m p o s i t e可以緩衝儲存實際結果或
者僅僅是一些用於縮短遍歷或查詢長度的資訊。例如,動機一節的例子中P i c t u r e類能高速緩
衝存貯其子部件的邊界框,在繪圖或選擇期間,當子部件在當前視窗中不可見時,這個邊界
框使得P i c t u r e不需要再進行繪圖或選擇。
一個元件發生變化時,它的父部件原先緩衝存貯的資訊也變得無效。在元件知道其父部
件時,這種方法最為有效。因此,如果你使用高速緩衝存貯,你需要定義一個介面來通知組
合組件它們所緩衝存貯的資訊無效。
8) 應該由誰刪除Component 在沒有垃圾回收機制的語言中,當一個C o m p o s i t e被銷燬時,
通常最好由C o m p o s i t e負責刪除其子節點。但有一種情況除外,即L e a f物件不會改變,因此可
以被共享。
9) 存貯元件最好用哪一種資料結構C o m p o s i t e可使用多種資料結構存貯它們的子節點,
包括連線列表、樹、陣列和h a s h表。資料結構的選擇取決於效率。事實上,使用通用資料結
構根本沒有必要。有時對每個子節點, C o m p o s i t e 都有一個變數與之對應,這就要求
C o m p o s i t e的每個子類都要實現自己的管理介面。參見I n t e r p r e t e r ( 5 . 3 )模式中的例子。
9. 程式碼示例
計算機和立體聲組合音響這樣的裝置經常被組裝成部分-整體層次結構或者是容器層次
結構。例如,底盤可包含驅動裝置和平面板,匯流排含有多個外掛,機櫃包括底盤、匯流排等。
這種結構可以很自然地用C o m p o s i t e模式進行模擬。
E q u i p m e n t類為在部分-整體層次結構中的所有裝置定義了一個介面。
1 1 2 設計模式:可複用面向物件軟體的基礎

Equipment 宣告一些操作返回一個裝置的屬性,例如它的能量消耗和價格。子類為指定的
裝置實現這些操作, E q u i p m e n t還聲明瞭一個C r e a t e I t e r a t o r操作,該操作為訪問它的零件返回
一個I t e r a t o r(參見附錄C)。這個操作的預設實現返回一個N u l l I t e r a t o r,它在空集上疊代。
Equipment 的子類包括表示磁碟驅動器、積體電路和開關的L e a f類:
CompositeEquipment 是包含其他裝置的基類,它也是E q u i p m e n t的子類。
C o m p o s i t e E q u i p m e n t為訪問和管理子裝置定義了一些操作。操作Add 和R e m o v e從儲存在
_ e q u i p m e n t成員變數中的裝置列表中插入並刪除裝置。操作C r e a t e I t e r a t o r返回一個迭代器
(L i s t I t e r a t o r的一個例項)遍歷這個列表。
N e t P r i c e的預設實現使用CreateIterator 來累加子裝置的實際價格。
現在我們將計算機的底盤表示為C o m p o s i t e E q u i p m e n t的子類C h a s s i s。C h a s s i s從
C o m p o s i t e E q u i p m e n t繼承了與子類有關的那些操作。
第4章結構型模式1 1 3

用完I t e r a t o r時,很容易忘記刪除它。I t e r a t o r模式描述瞭如何處理這類問題。
我們可用相似的方式定義其他裝置容器,如C a b i n e t和B u s。這樣我們就得到了組裝一臺
(非常簡單)個人計算機所需的所有裝置。
10. 已知應用
幾乎在所有面向物件的系統中都有Composite 模式的應用例項。在S m a l l t a l k中的
M o d e l / Vi e w / C o n t r o l l e r [ K P 8 8 ]結構中,原始Vi e w類就是一個Composite, 幾乎每個使用者介面工
具箱或框架都遵循這些步驟,其中包括ET++ ( 用V O b j e c t s [ W G M 8 8 ] )和I n t e r Vi e w s ( S t y l e
[ L C I + 9 2 ] , G r a p h i c s [ V L 8 8 ]和G l y p h s [ C L 9 0 ] )。很有趣的是Model /Vi e w / C o n t r o l l e r中的原始Vi e w
有一組子檢視;換句話說, View 既是Component 類,又是C o m p o s i t e類。4 . 0版的S m a l l t a l k - 8 0
用Vi s u a l C o m p o n e n t類修改了M o d e l / Vi e w / C o n t r o l l e r, Vi s u a l C o m p o n e n t類含有子類Vi e w和
C o m p o s i t e Vi e w。
RTL Smalltalk 編譯器框架[ J M L 9 2 ]大量地使用了C o m p o s i t e模式。RTLExpression 是一個
對應於語法分析樹的C o m p o n e n t 類。它有一些子類,例如B i n a r y E x p r e s s i o n ,而
B i n a r y E x p r e s s i o n包含子RT L E x p r e s s i o n物件。這些類為語法分析樹定義了一個組合結構。
R e g i s t e r Tr a n s f e r是一個用於程式的中間Single Static Assignment(SSA)形式的Component 類。
R e g i s t e r Tr a n s f e r的L e a f子類定義了一些不同的靜態賦值形式,例如:
• 基本賦值,在兩個暫存器上執行操作並且將結果放入第三個暫存器中。
• 具有源暫存器但無目標暫存器的賦值,這說明是在例程返回後使用該暫存器。
• 具有目標暫存器但無源暫存器的賦值,這說明是在例程開始之前分配目標暫存器。
另一個子類R e g i s t e r Tr a n s f e r S e t,是一個C o m p o s i t e類,表示一次改變幾個暫存器的賦值。
這種模式的另一個例子出現在財經應用領域,在這一領域中,一個資產組合聚合多個單
個資產。為了支援複雜的資產聚合,資產組合可以用一個C o m p o s i t e類實現,這個C o m p o s i t e
類與單個資產的介面一致[ B E 9 3 ]。
C o m m a n d(5 . 2)模式描述瞭如何用一個MacroCommand Composite類組成一些C o m m a n d
物件,並對它們進行排序。
11. 相關模式
通常部件-父部件連線用於Responsibility of Chain(5.1)模式。
D e c o r a t o r(4 . 4)模式經常與C o m p o s i t e模式一起使用。當裝飾和組合一起使用時,它們
通常有一個公共的父類。因此裝飾必須支援具有A d d、R e m o v e和GetChild 操作的C o m p o n e n t
1 1 4 設計模式:可複用面向物件軟體的基礎

介面。
F l y w e i g h t ( 4 . 6 )讓你共享元件,但不再能引用他們的父部件。
I t e r t o r ( 5 . 4 )可用來遍歷C o m p o s i t e。
Vi s i t o r ( 5 . 11 )將本來應該分佈在C o m p o s i t e和L e a f類中的操作和行為區域性化。
4.4 DECORATOR(裝飾)—物件結構型模式
1. 意圖
動態地給一個物件新增一些額外的職責。就增加功能來說, D e c o r a t o r模式相比生成子類
更為靈活。
2. 別名
包裝器Wr a p p e r
3. 動機
有時我們希望給某個物件而不是整個類新增一些功能。例如,一個圖形使用者介面工具箱
允許你對任意一個使用者介面元件新增一些特性,例如邊框,或是一些行為,例如視窗滾動。
使用繼承機制是新增功能的一種有效途徑,從其他類繼承過來的邊框特性可以被多個子
類的例項所使用。但這種方法不夠靈活,因為邊框的選擇是靜態的,使用者不能控制對元件加
邊框的方式和時機。
一種較為靈活的方式是將元件嵌入另一個物件中,由這個物件新增邊框。我們稱這個嵌
入的物件為裝飾。這個裝飾與它所裝飾的元件介面一致,因此它對使用該元件的客戶透明。
它將客戶請求轉發給該元件,並且可能在轉發前後執行一些額外的動作(例如畫一個邊框)。
透明性使得你可以遞迴的巢狀多個裝飾,從而可以新增任意多的功能,如下圖所示。
例如,假定有一個物件Te x t Vi e w,它可以在視窗中顯示正文。預設的Te x t Vi e w沒有滾動
條,因為我們可能有時並不需要滾動條。當需要滾動條時,我們可以用S c r o l l D e c o r a t o r新增滾
動條。如果我們還想在Te x t Vi e w周圍新增一個粗黑邊框,可以使用B o r d e r D e c o r a t o r新增。因
此只要簡單地將這些裝飾和Te x t Vi e w進行組合,就可以達到預期的效果。
下面的物件圖展示瞭如何將一個Te x t Vi e w物件與B o r d e r D e c o r a t o r以及S c r o l l D e c o r a t o r物件
組裝起來產生一個具有邊框和滾動條的文字顯示視窗。
第4章結構型模式1 1 5

S c r o l l D e c o r a t o r和BorderDecorator 類是D e c o r a t o r類的子類。D e c o r a t o r類是一個可視元件
的抽象類,用於裝飾其他可視元件,如下圖所示。
Vi s u a l C o m p o n e n t是一個描述可視物件的抽象類,它定義了繪製和事件處理的介面。注意
D e c o r a t o r類怎樣將繪製請求簡單地傳送給它的元件,以及D e c o r a t o r的子類如何擴充套件這個操作。
D e c o r a t o r的子類為特定功能可以自由地新增一些操作。例如,如果其他物件知道介面中
恰好有一個S c r o l l D e c o r a t o r物件,這些物件就可以用S c r o l l D e c o r a t o r物件的S c r o l l To操作滾動這
個介面。這個模式中有一點很重要,它使得在Vi s u a l C o m p o n e n t可以出現的任何地方都可以有
裝飾。因此,客戶通常不會感覺到裝飾過的元件與未裝飾元件之間的差異,也不會與裝飾產
生任何依賴關係。
4. 適用性
以下情況使用D e c o r a t o r模式
• 在不影響其他物件的情況下,以動態、透明的方式給單個物件新增職責。
• 處理那些可以撤消的職責。
• 當不能採用生成子類的方法進行擴充時。一種情況是,可能有大量獨立的擴充套件,為支援
每一種組合將產生大量的子類,使得子類數目呈爆炸性增長。另一種情況可能是因為類
定義被隱藏,或類定義不能用於生成子類。
5. 結構
1 1 6 設計模式:可複用面向物件軟體的基礎

6. 參與者
• Component ( Vi s u a l C o m p o n e n t )
— 定義一個物件介面,可以給這些物件動態地新增職責。
• C o n c r e t e C o m p o n e n t ( Te x t Vi e w )
— 定義一個物件,可以給這個物件新增一些職責。
• D e c o r a t o r
— 維持一個指向C o m p o n e n t物件的指標,並定義一個與C o m p o n e n t介面一致的介面。
• C o n c r e t e D e c o r a t o r ( B o r d e r D e c o r a t o r, ScrollDecorator)
— 向元件新增職責。
7. 協作
• D e c o r a t o r將請求轉發給它的C o m p o n e n t物件,並有可能在轉發請求前後執行一些附加的
動作。
8. 效果
D e c o r a t o r模式至少有兩個主要優點和兩個缺點:
1) 比靜態繼承更靈活與物件的靜態繼承(多重繼承)相比, D e c o r a t o r模式提供了更加
靈活的向物件新增職責的方式。可以用新增和分離的方法,用裝飾在執行時刻增加和刪除職
責。相比之下,繼承機制要求為每個新增的職責建立一個新的子類(例如, B o r d e r S c r o l l a b l e
Te x t Vi e w, BorderedTe x t Vi e w)。這會產生許多新的類,並且會增加系統的複雜度。此外,為一
個特定的C o m p o n e n t類提供多個不同的D e c o r a t o r類,這就使得你可以對一些職責進行混合和
匹配。
使用D e c o r a t o r模式可以很容易地重複新增一個特性,例如在Te x t Vi e w上新增雙邊框時,
僅需將新增兩個B o r d e r D e c o r a t o r即可。而兩次繼承B o r d e r類則極容易出錯的。
2) 避免在層次結構高層的類有太多的特徵D e c o r a t o r模式提供了一種“即用即付”的方
法來新增職責。它並不試圖在一個複雜的可定製的類中支援所有可預見的特徵,相反,你可
以定義一個簡單的類,並且用D e c o r a t o r類給它逐漸地新增功能。可以從簡單的部件組合出復
雜的功能。這樣,應用程式不必為不需要的特徵付出代價。同時也更易於不依賴於D e c o r a t o r
所擴充套件(甚至是不可預知的擴充套件)的類而獨立地定義新型別的D e c o r a t o r。擴充套件一個複雜類的
時候,很可能會暴露與新增的職責無關的細節。
3) Decorator與它的C o m p o n e n t不一樣D e c o r a t o r是一個透明的包裝。如果我們從物件標
識的觀點出發,一個被裝飾了的元件與這個元件是有差別的,因此,使用裝飾時不應該依賴
物件標識。
4) 有許多小物件採用D e c o r a t o r模式進行系統設計往往會產生許多看上去類似的小物件,
這些物件僅僅在他們相互連線的方式上有所不同,而不是它們的類或是它們的屬性值有所不