天天看點

java設計模式之組合模式

學習難度:★★★☆☆,使用頻率:★★★★☆】 

樹形結構在軟體中随處可見,例如作業系統中的目錄結構、應用軟體中的菜單、辦公系統中的公司組織結構等等,如何運用面向對象的方式來處理這種樹形結構是組合模式需要解決的問題,組合模式通過一種巧妙的設計方案使得使用者可以一緻性地處理整個樹形結構或者樹形結構的一部分,也可以一緻性地處理樹形結構中的葉子節點(不包含子節點的節點)和容器節點(包含子節點的節點)。下面将學習這種用于處理樹形結構的組合模式。

      Sunny軟體公司欲開發一個殺毒(AntiVirus)軟體,該軟體既可以對某個檔案夾(Folder)殺毒,也可以對某個指定的檔案(File)進行殺毒。該防毒軟體還可以根據各類檔案的特點,為不同類型的檔案提供不同的殺毒方式,例如圖像檔案(ImageFile)和文本檔案(TextFile)的殺毒方式就有所差異。現需要提供該防毒軟體的整體架構設計方案。

      在介紹Sunny公司開發人員提出的初始解決方案之前,我們先來分析一下作業系統中的檔案目錄結構,例如在Windows作業系統中,存在如圖11-1所示目錄結構:

圖11-1 Windows目錄結構

      圖11-1可以簡化為如圖11-2所示樹形目錄結構:

圖11-2 樹形目錄結構示意圖

      我們可以看出,在圖11-2中包含檔案(灰色節點)和檔案夾(白色節點)兩類不同的元素,其中在檔案夾中可以包含檔案,還可以繼續包含子檔案夾,但是在檔案中不能再包含子檔案或者子檔案夾。在此,我們可以稱檔案夾為容器(Container),而不同類型的各種檔案是其成員,也稱為葉子(Leaf),一個檔案夾也可以作為另一個更大的檔案夾的成員。如果我們現在要對某一個檔案夾進行操作,如查找檔案,那麼需要對指定的檔案夾進行周遊,如果存在子檔案夾則打開其子檔案夾繼續周遊,如果是檔案則判斷之後傳回查找結果。

      Sunny軟體公司的開發人員通過分析,決定使用面向對象的方式來實作對檔案和檔案夾的操作,定義了如下圖像檔案類ImageFile、文本檔案類TextFile和檔案夾類Folder:

編寫如下用戶端測試代碼進行測試:

編譯并運作程式,輸出結果如下:

****對檔案夾'Sunny的資料'進行殺毒

****對檔案夾'圖像檔案'進行殺毒

----對圖像檔案'小龍女.jpg'進行殺毒

----對圖像檔案'張無忌.gif'進行殺毒

****對檔案夾'文本檔案'進行殺毒

----對文本檔案'九陰真經.txt'進行殺毒

----對文本檔案'葵花寶典.doc'進行殺毒

      Sunny公司開發人員“成功”實作了防毒軟體的架構設計,但通過仔細分析,發現該設計方案存在如下問題:

      (1) 檔案夾類Folder的設計和實作都非常複雜,需要定義多個集合存儲不同類型的成員,而且需要針對不同的成員提供增加、删除和擷取等管理和通路成員的方法,存在大量的備援代碼,系統維護較為困難;

      (2) 由于系統沒有提供抽象層,用戶端代碼必須有差別地對待充當容器的檔案夾Folder和充當葉子的ImageFile和TextFile,無法統一對它們進行處理;

      (3) 系統的靈活性和可擴充性差,如果需要增加新的類型的葉子和容器都需要對原有代碼進行修改,例如如果需要在系統中增加一種新類型的視訊檔案VideoFile,則必須修改Folder類的源代碼,否則無法在檔案夾中添加視訊檔案。

      面對以上問題,Sunny軟體公司的開發人員該如何來解決?這就需要用到本章将要介紹的組合模式,組合模式為處理樹形結構提供了一種較為完美的解決方案,它描述了如何将容器和葉子進行遞歸組合,使得使用者在使用時無須對它們進行區分,可以一緻地對待容器和葉子。

      對于樹形結構,當容器對象(如檔案夾)的某一個方法被調用時,将周遊整個樹形結構,尋找也包含這個方法的成員對象(可以是容器對象,也可以是葉子對象)并調用執行,牽一而動百,其中使用了遞歸調用的機制來對整個結構進行處理。由于容器對象和葉子對象在功能上的差別,在使用這些對象的代碼中必須有差別地對待容器對象和葉子對象,而實際上大多數情況下我們希望一緻地處理它們,因為對于這些對象的差別對待将會使得程式非常複雜。組合模式為解決此類問題而誕生,它可以讓葉子對象和容器對象的使用具有一緻性。

      組合模式定義如下:

組合模式(Composite Pattern):組合多個對象形成樹形結構以表示具有“整體—部分”關系的層次結構。組合模式對單個對象(即葉子對象)群組合對象(即容器對象)的使用具有一緻性,組合模式又可以稱為“整體—部分”(Part-Whole)模式,它是一種對象結構型模式。

      在組合模式中引入了抽象構件類Component,它是所有容器類和葉子類的公共父類,用戶端針對Component進行程式設計。組合模式結構如圖11-3所示:

圖11-3  組合模式結構圖

      在組合模式結構圖中包含如下幾個角色:

      ● Component(抽象構件):它可以是接口或抽象類,為葉子構件和容器構件對象聲明接口,在該角色中可以包含所有子類共有行為的聲明和實作。在抽象構件中定義了通路及管理它的子構件的方法,如增加子構件、删除子構件、擷取子構件等。

      ● Leaf(葉子構件):它在組合結構中表示葉子節點對象,葉子節點沒有子節點,它實作了在抽象構件中定義的行為。對于那些通路及管理子構件的方法,可以通過異常等方式進行處理。

      ● Composite(容器構件):它在組合結構中表示容器節點對象,容器節點包含子節點,其子節點可以是葉子節點,也可以是容器節點,它提供一個集合用于存儲子節點,實作了在抽象構件中定義的行為,包括那些通路及管理子構件的方法,在其業務方法中可以遞歸調用其子節點的業務方法。

      組合模式的關鍵是定義了一個抽象構件類,它既可以代表葉子,又可以代表容器,而用戶端針對該抽象構件類進行程式設計,無須知道它到底表示的是葉子還是容器,可以對其進行統一處理。同時容器對象與抽象構件類之間還建立一個聚合關聯關系,在容器對象中既可以包含葉子,也可以包含容器,以此實作遞歸組合,形成一個樹形結構。

      如果不使用組合模式,用戶端代碼将過多地依賴于容器對象複雜的内部實作結構,容器對象内部實作結構的變化将引起客戶代碼的頻繁變化,帶來了代碼維護複雜、可擴充性差等弊端。組合模式的引入将在一定程度上解決這些問題。

      下面通過簡單的示例代碼來分析組合模式的各個角色的用途和實作。對于組合模式中的抽象構件角色,其典型代碼如下所示:

一般将抽象構件類設計為接口或抽象類,将所有子類共有方法的聲明和實作放在抽象構件類中。對于用戶端而言,将針對抽象構件程式設計,而無須關心其具體子類是容器構件還是葉子構件。

      如果繼承抽象構件的是葉子構件,則其典型代碼如下所示:

作為抽象構件類的子類,在葉子構件中需要實作在抽象構件類中聲明的所有方法,包括業務方法以及管理和通路子構件的方法,但是葉子構件不能再包含子構件,是以在葉子構件中實作子構件管理和通路方法時需要提供異常處理或錯誤提示。當然,這無疑會給葉子構件的實作帶來麻煩。

      如果繼承抽象構件的是容器構件,則其典型代碼如下所示:

 在容器構件中實作了在抽象構件中聲明的所有方法,既包括業務方法,也包括用于通路和管理成員子構件的方法,如add()、remove()和getChild()等方法。需要注意的是在實作具體業務方法時,由于容器構件充當的是容器角色,包含成員構件,是以它将調用其成員構件的業務方法。在組合模式結構中,由于容器構件中仍然可以包含容器構件,是以在對容器構件進行處理時需要使用遞歸算法,即在容器構件的operation()方法中遞歸調用其成員構件的operation()方法。

思考

      在組合模式結構圖中,如果聚合關聯關系不是從Composite到Component的,而是從Composite到Leaf的,如圖11-4所示,會産生怎樣的結果?

圖11-4   組合模式思考題結構圖

      為了讓系統具有更好的靈活性和可擴充性,用戶端可以一緻地對待檔案和檔案夾,Sunny公司開發人員使用組合模式來進行防毒軟體的架構設計,其基本結構如圖11-5所示:

圖11-5  防毒軟體架構設計結構圖

    在圖11-5中, AbstractFile充當抽象構件類,Folder充當容器構件類,ImageFile、TextFile和VideoFile充當葉子構件類。完整代碼如下所示:

 編寫如下用戶端測試代碼:

 編譯并運作程式,輸出結果如下:

****對檔案夾'視訊檔案'進行殺毒

----對視訊檔案'笑傲江湖.rmvb'進行殺毒

      由于在本執行個體中使用了組合模式,在抽象構件類中聲明了所有方法,包括用于管理和通路子構件的方法,如add()方法和remove()方法等,是以在ImageFile等葉子構件類中實作這些方法時必須進行相應的異常處理或錯誤提示。在容器構件類Folder的killVirus()方法中将遞歸調用其成員對象的killVirus()方法,進而實作對整個樹形結構的周遊。

      如果需要更換操作節點,例如隻需對檔案夾“文本檔案”進行殺毒,用戶端代碼隻需修改一行即可,将代碼:

folder1.killVirus();

       改為:

folder3.killVirus();

       輸出結果如下:

       在具體實作時,我們可以建立圖形化界面讓使用者選擇所需操作的根節點,無須修改源代碼,符合“開閉原則”,用戶端無須關心節點的層次結構,可以對所選節點進行統一處理,提高系統的靈活性。

      通過引入組合模式,Sunny公司設計的防毒軟體具有良好的可擴充性,在增加新的檔案類型時,無須修改現有類庫代碼,隻需增加一個新的檔案類作為AbstractFile類的子類即可,但是由于在AbstractFile中聲明了大量用于管理和通路成員構件的方法,例如add()、remove()等方法,我們不得不在新增的檔案類中實作這些方法,提供對應的錯誤提示和異常處理。為了簡化代碼,我們有以下兩個解決方案:

      解決方案一:将葉子構件的add()、remove()等方法的實作代碼移至AbstractFile類中,由AbstractFile提供統一的預設實作,代碼如下所示:

如果用戶端代碼針對抽象類AbstractFile程式設計,在調用檔案對象的這些方法時将出現錯誤提示。如果不希望出現任何錯誤提示,我們可以在用戶端定義檔案對象時不使用抽象層,而直接使用具體葉子構件本身,用戶端代碼片段如下所示:

  這樣就産生了一種不透明的使用方式,即在用戶端不能全部針對抽象構件類程式設計,需要使用具體葉子構件類型來定義葉子對象。

      解決方案二:除此之外,還有一種解決方法是在抽象構件AbstractFile中不聲明任何用于通路和管理成員構件的方法,代碼如下所示:

此時,由于在AbstractFile中沒有聲明add()、remove()等通路和管理成員的方法,其葉子構件子類無須提供實作;而且無論用戶端如何定義葉子構件對象都無法調用到這些方法,不需要做任何錯誤和異常處理,容器構件再根據需要增加通路和管理成員的方法,但這時候也存在一個問題:用戶端不得不使用容器類本身來聲明容器構件對象,否則無法通路其中新增的add()、remove()等方法,如果用戶端一緻性地對待葉子和容器,将會導緻容器構件的新增對用戶端不可見,用戶端代碼對于容器構件無法再使用抽象構件來定義,用戶端代碼片段如下所示:

 在使用組合模式時,根據抽象構件類的定義形式,我們可将組合模式分為透明組合模式和安全組合模式兩種形式:

      (1) 透明組合模式

      透明組合模式中,抽象構件Component中聲明了所有用于管理成員對象的方法,包括add()、remove()以及getChild()等方法,這樣做的好處是確定所有的構件類都有相同的接口。在用戶端看來,葉子對象與容器對象所提供的方法是一緻的,用戶端可以相同地對待所有的對象。透明組合模式也是組合模式的标準形式,雖然上面的解決方案一在用戶端可以有不透明的實作方法,但是由于在抽象構件中包含add()、remove()等方法,是以它還是透明組合模式,透明組合模式的完整結構如圖11-6所示:

圖11-6  透明組合模式結構圖

      透明組合模式的缺點是不夠安全,因為葉子對象和容器對象在本質上是有差別的。葉子對象不可能有下一個層次的對象,即不可能包含成員對象,是以為其提供add()、remove()以及getChild()等方法是沒有意義的,這在編譯階段不會出錯,但在運作階段如果調用這些方法可能會出錯(如果沒有提供相應的錯誤處理代碼)。

      (2) 安全組合模式

      安全組合模式中,在抽象構件Component中沒有聲明任何用于管理成員對象的方法,而是在Composite類中聲明并實作這些方法。這種做法是安全的,因為根本不向葉子對象提供這些管理成員對象的方法,對于葉子對象,用戶端不可能調用到這些方法,這就是解決方案二所采用的實作方式。安全組合模式的結構如圖11-7所示:

圖11-7  安全組合模式結構圖

       安全組合模式的缺點是不夠透明,因為葉子構件和容器構件具有不同的方法,且容器構件中那些用于管理成員對象的方法沒有在抽象構件類中定義,是以用戶端不能完全針對抽象程式設計,必須有差別地對待葉子構件和容器構件。在實際應用中,安全組合模式的使用頻率也非常高,在Java AWT中使用的組合模式就是安全組合模式。

       在學習和使用組合模式時,Sunny軟體公司開發人員發現樹形結構其實随處可見,例如Sunny公司的組織結構就是“一棵标準的樹”,如圖11-8所示:

圖11-8  Sunny公司組織結構圖

      在Sunny軟體公司的内部辦公系統Sunny OA系統中,有一個與公司組織結構對應的樹形菜單,行政人員可以給各級機關下發通知,這些機關可以是總公司的一個部門,也可以是一個分公司,還可以是分公司的一個部門。使用者隻需要選擇一個根節點即可實作通知的下發操作,而無須關心具體的實作細節。這不正是組合模式的“特長”嗎?于是Sunny公司開發人員繪制了如圖11-9所示結構圖:

圖11-9  Sunny公司組織結構組合模式示意圖

       在圖11-9中,“機關”充當了抽象構件角色,“公司”充當了容器構件角色,“研發部”、“财務部”和“人力資源部”充當了葉子構件角色。

如何編碼實作圖11-9中的“公司”類?

      組合模式使用面向對象的思想來實作樹形結構的建構與處理,描述了如何将容器對象和葉子對象進行遞歸組合,實作簡單,靈活性好。由于在軟體開發中存在大量的樹形結構,是以組合模式是一種使用頻率較高的結構型設計模式,Java SE中的AWT和Swing包的設計就基于組合模式,在這些界面包中為使用者提供了大量的容器構件(如Container)和成員構件(如Checkbox、Button和TextComponent等),其結構如圖11-10所示:

圖11-10 AWT組合模式結構示意圖

      在圖11-10中,Component類是抽象構件,Checkbox、Button和TextComponent是葉子構件,而Container是容器構件,在AWT中包含的葉子構件還有很多,因為篇幅限制沒有在圖中一一列出。在一個容器構件中可以包含葉子構件,也可以繼續包含容器構件,這些葉子構件和容器構件一起組成了複雜的GUI界面。

      除此以外,在XML解析、組織結構樹處理、檔案系統設計等領域,組合模式都得到了廣泛應用。

      1. 主要優點

      組合模式的主要優點如下:

      (1) 組合模式可以清楚地定義分層次的複雜對象,表示對象的全部或部分層次,它讓用戶端忽略了層次的差異,友善對整個層次結構進行控制。

      (2) 用戶端可以一緻地使用一個組合結構或其中單個對象,不必關心處理的是單個對象還是整個組合結構,簡化了用戶端代碼。

      (3) 在組合模式中增加新的容器構件和葉子構件都很友善,無須對現有類庫進行任何修改,符合“開閉原則”。

      (4) 組合模式為樹形結構的面向對象實作提供了一種靈活的解決方案,通過葉子對象和容器對象的遞歸組合,可以形成複雜的樹形結構,但對樹形結構的控制卻非常簡單。

      2. 主要缺點

      組合模式的主要缺點如下:

      在增加新構件時很難對容器中的構件類型進行限制。有時候我們希望一個容器中隻能有某些特定類型的對象,例如在某個檔案夾中隻能包含文本檔案,使用組合模式時,不能依賴類型系統來施加這些限制,因為它們都來自于相同的抽象層,在這種情況下,必須通過在運作時進行類型檢查來實作,這個實作過程較為複雜。

      3. 适用場景

      在以下情況下可以考慮使用組合模式:

      (1) 在具有整體和部分的層次結構中,希望通過一種方式忽略整體與部分的差異,用戶端可以一緻地對待它們。

      (2) 在一個使用面向對象語言開發的系統中需要處理一個樹形結構。

      (3) 在一個系統中能夠分離出葉子對象和容器對象,而且它們的類型不固定,需要增加一些新的類型。

練習

Sunny軟體公司欲開發一個界面控件庫,界面控件分為兩大類,一類是單元控件,例如按鈕、文本框等,一類是容器控件,例如窗體、中間面闆等,試用組合模式設計該界面控件庫。

【作者:劉偉  http://blog.csdn.net/lovelion】