天天看點

COMPOSITE(組合)模式

1、意圖

        将對象組合成樹形結構以表示“部分-整體”的層次結構。Composite模式使得使用者對單個對象群組合對象的使用具有一緻性。

2、動機

        在繪圖編輯器和圖形捕捉系統這樣的圖形應用程式中,使用者可以使用簡單的元件建立複雜的圖表。使用者可以組合多個簡單的元件以形成一些較大的元件,這些元件又可以組合成更大的元件。一個簡單的實作方法是為Text和Line這樣的圖元定義一些類,另外定義一些類作為這些圖元的容器類。

        然而這種方法存在一個問題:使用這些類的代碼必須差別對待圖元對象和容器對象,而大多數情況下使用者認為他們是一樣的。對這些類差別使用,使得程式更加複雜。

        Composite模式描述了如何使用遞歸組合,使得使用者不必對這些類進行差別,如下圖所示:

COMPOSITE(組合)模式

        Composite的關鍵是一個抽象類,它既可以代表圖元又可以代表圖元的容器。在圖形系統中的這個類就是Graphic,它聲明一些與特定圖形對象相關的操作,例如Draw。同時它也聲明了所有的組合對象共享的一些操作,例如一些操作用于通路和管理它的子部件。

        子類Line、Rectangle和Text(參見前面的類圖)定義了一些圖元對象,這些類實作Draw,分别用于繪制直線、矩形和正文。由于圖元都沒有子類型,是以他們都不執行與子類有關的操作。

        Picture類定義了一個Graphic對象的聚合。Picture的Draw操作是通過對它的子部件調用Draw實作的,Picture還用這種方法實作了一些與子部件相關的操作。由于Picture接口與Graphic接口是一緻的,是以Picture對象可以遞歸地組合其它Picture對象。

        下圖是一個典型的由遞歸組合的Graphic對象組成的組合對象結構:

COMPOSITE(組合)模式

3、适用性

        以下情況使用Composite模式

        · 你想表示對象的部分-整體層次結構;

        · 你希望使用者忽略組合對象和單個對象的不同,使用者将統一使用組合結構中的所有對象。

4、結構

COMPOSITE(組合)模式

        典型的Composite對象結構如下圖所示:

COMPOSITE(組合)模式

5、參與者

        · Component(Graphic)

          -- 為組合中的對象聲明接口。

          -- 在适當情況下,實作所有類共有接口的預設行為。

          -- 聲明一個接口使用者通路和管理Component的子元件。

          -- (可選)在遞歸結構中定義一個接口,用于通路一個父部件,并在合适的情況下實作它。

        · Leaf(Rectangle、Line、Text等)

          -- 在組合中表示葉節點對象,葉節點沒有子節點。

          -- 在組合中定義圖元對象的行為。

        · Composite(Picture)

          -- 定義有子部件的那些部件的行為。

          -- 存儲子部件。

          -- 在Component接口中實作與子部件有關的操作。

        · Client

          -- 通過Component接口操縱組合部件的對象。

6、協作

        使用者使用Component類接口與組合結構中的對象進行互動。如果接受者是個葉節點,則直接處理請求。如果接受者是Composite,它通常将請求發送給它的子部件,在轉發請求之前或之後可能執行一些輔助操作。

7、效果

        Composite模式:

        · 定義了包含基本對象群組合對象的類層次結構    基本對象可以被組合成更複雜的組合對象,而這個組合對象又可以被組合,這樣不斷地遞歸下去。客戶代碼中,任何用到基本對象的地方都可以使用組合對象。

        · 簡化客戶代碼    客戶可以一緻地使用組合結構和單個對象。通常使用者不知道(也不關心)處理的是一個葉節點還是一個組合元件。這就簡化了客戶代碼,因為在定義組合的那些類中不需要寫一些充斥着選擇語句的函數。

        · 使得更容易增加新類型的元件    新定義的Composite或Leaf子類自動地與已有的結構和客戶代碼一起工作,客戶程式不需因新的Component類而改變。

        · 使你的設計變得更加一般化    容易增加新元件也會産生一些問題,那就是很難限制組合中的元件。有時你希望一個組合隻能有某些特定的元件。使用Composite時,你不能依賴類型系統施加這些限制,而必須在運作時刻進行檢查。

8、實作

        我們在實作Composite模式時需要考慮以下幾個問題:

        1)顯式的父部件引用    保持從子部件到父部件的引用能簡化組合結構的周遊和管理。父部件的引用可以簡化結構的上移群組件的删除,同時父部件引用也支援ChainOfResponsibility模式。

          通常在Component類中定義父部件引用。Leaf和Composite類可以繼承這個引用以及管理這個引用的那些操作。

          對于父部件引用,必須維護一個不變式,即一個組合的所有子節點以這個組合為父節點,而反之該組合以這些節點為子節點。保證這一點最容易的辦法是,僅當在一個組合中增加或删除一個元件時,才改變這個元件的父部件。如果能在Composite類的Add和Remove操作中實作這種方法,那麼所有的子類都可以繼承這一方法,并且将自動維護這一不變式。

        2)共享元件    共享元件是很有用的,比如它可以減少對存貯的需求。但是當一個元件隻有一個父元件時,很難共享元件。

          一個可行的解決辦法是為子部件存貯多個父部件,但當一個請求在結構中向上傳遞時,這種方法會導緻多義性。Flyweight模式讨論了如何修改設計以避免将父部件存貯在一起的方法。如果子部件可以将一些狀态(或是所有的狀态)存儲在外部,進而不需要向父部件發送請求,那麼這種方法是可行的。

        3)最大化Component接口    Composite模式的目的之一是使得使用者不知道他們正在使用的具體的Leaf和Composite類。為了達到這一目的,Composite類應為Leaf和Composite類盡可能多定義一些公共操作。Composite類通常為這些操作提供預設的實作,而Leaf和Composite子類可以對他們進行重定義。

          燃熱,這個目标有時可能會與類層次結構設計原則相沖突,該原則規定:一個類隻能定義那些對它的子類有意義的操作。有許多Component所支援的操作對Leaf類似乎沒有什麼意義,那麼Component怎樣為他們提供一個預設的操作呢?

          有時一點穿造型可以使得一個看起來僅對Composite才有意義的操作,将它移入Component類中,就會對所有的Component都适用。例如,通路子節點的接口是Composite類的一個基本組成部分,但對Leaf類來說并不必要。但是如果我們把一個Leaf看成一個沒有子節點的Component就可以為這個Component類中定義一個預設的操作,用于對子節點進行通路,這個預設的操作不傳回任何一個子節點。Leaf類可以适用預設的實作,而Composite類則會重新實作這個操作以傳回它們的子類。

          管理子部件的操作比較複雜,我們将在下一項中予以讨論。

        4)聲明管理子部件的操作    雖然Composite類實作了Add和Remove操作用于管理子部件,但在Composite模式中一個重要的問題是:在Composite類層次結構中哪一些類聲明這些操作。我們是應該在Component中聲明這些操作,并使這些操作對Leaf類有意義呢,還是隻應該在Composite和它的子類中聲明并定義這些操作呢?

          這需要在安全性和透明性之間做出權衡選擇。

          · 在類層次結構的根部定義子節點管理接口的方法具有良好的透明性,因為你可以一緻地使用所有的元件,但是這一方法是以安全性為代價的,因為客戶有可能會做一些無意義的事情,例如在Leaf中增加和删除對象等。

          · 在Composite類中定義管理子部件的方法具有良好的安全性,因為在像C++這樣的靜态類語言中,在編譯時任何從Leaf中增加或删除對象的嘗試都将被發現。但是這又損失了透明性因為leaf和Composite具有不同的接口。

          在這一模式中,相對于安全性,我們比較強調透明性。如果你選擇了安全性,有時你可能會丢失類型資訊,并且不得不将一個元件轉換成一個組合。這樣的類型轉換必定不是類型安全的。

          一種辦法是在Component類中聲明一個操作Composite* GetComposite()。Component提供了一個傳回空指針的預設操作。Composite類重新定義這個操作并通過this指針傳回它自身。GetComposite允許你查詢一個元件看它是否是一個組合,你可以對傳回的組合安全地執行Add和Remove操作。

          當然,這裡的問題是我們對所有的元件的處理并不一緻。在進行适當的動作之前,我們必須檢測不同的類型。

          提供透明性的唯一方法是在Component中定義預設Add和Remove操作。這又帶來了一個新的問題:Component::Add的實作不可避免地會有失敗的可能性。你可以不讓Component::Add做任何事情,但這就忽略了一個很重要的問題:企圖向葉節點中增加一些東西時可能會引入錯誤。這時Add操作會産生垃圾。你可以讓Add操作删除它的參數,但可能客戶并不希望這樣。

          如果該元件不允許有子部件,或者Remove的參數不是該元件的子節點時,通常最好使用預設方式(可能是産生一個異常)處理Add和Remove的失敗。

          另一個辦法是對“删除”的含義做一些改變。如果該元件有一個父部件引用,我們可重新定義Component::Remove,在它的父元件中删除掉這個元件。然而,對應的Add操作仍然沒有合理的解釋。

        5)Component是否應該實作一個Component清單    你可能希望在Component類中将子節點集合定義為一個執行個體變量,而這個Component類中也聲明了一些操作對子節點進行通路和管理。但是在基類中存放子類指針,對葉節點來說會導緻空間浪費,因為葉節點根本沒有子節點。隻有當該結構中子類數目相對較少時,才值得使用這種方法。

        6)子部件排序    許多設計制定了Composite的子部件順序。在前面的Graphics例子中,排序可能表示了從前至後的順序。如果Composite表示文法分析樹,Composite子部件的順序必須反映程式結構,而組合語句就是這樣一些Composite的執行個體。

          如果需要考慮子節點的順序時,必須仔細地設計對子節點的通路和管理接口,以便管理子節點序列。Iterator模式可以在這方面給予一些指導。

        7)使用高速緩沖存貯改善性能    如果你需要對組合進行頻繁的周遊或查找,Composite類可以緩沖存儲對它的子節點進行周遊或查找的相關資訊。Composite可以緩沖存儲實際結構或者僅僅是一些用于縮短周遊或查詢長度的資訊。例如,動機一節的例子中Picture類能告訴緩沖存儲其子部件的邊界框,在繪圖或選擇期間,當子部件在目前視窗中不可見時,這個邊界框使得Picture不需要再進行繪圖或選擇。

          一個元件發生變化時,他的父部件原先緩沖存貯的資訊也變得無效。在元件知道其父部件時,這種方法最為有效。是以,如果你使用告訴緩沖存貯,你需要定義一個接口來通知組合元件它們所緩沖存貯的資訊無效。

        8)應該由誰删除Component    在沒有垃圾回收機制的語言中,當一個Composite被銷毀時,通常最好由Composite負責删除其子節點。但有一種情況除外,即Leaf對象不會改變,是以可以被共享。

        9)存貯元件最好用哪一種資料結構    Composite可使用多種資料結構存貯它們的子節點,包括連接配接清單、樹、數組和hash表。資料結構的選擇取決于效率。事實上,使用通用化素具結構根本沒有必要。有時對每個子節點,Composite都有一個變量與之對應,這就要求Composite的每個子類都要實作自己的管理接口。參見Interpreter模式中的例子。

9、代碼示例

        網絡上java的組合模式的代碼示例有很多,可以随便參考。GOF的設計模式中使用的C++代碼,此處不再列出。

繼續閱讀