JAVA設計模式(10):結構型-組合模式(Composite)

taozihk發表於2016-05-17

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

 

1 設計防毒軟體的框架結構

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

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

1 Windows目錄結構

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

樹形目錄結構示意圖

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

      Sunny軟體公司的開發人員通過分析,決定使用物件導向的方式來實現對檔案和資料夾的操作,定義瞭如下影像檔案類ImageFile、文字檔案類TextFile和資料夾類Folder

[java] view plaincopy
  1. //為了突出核心框架程式碼,我們對防毒過程的實現進行了大量簡化  
  2. import java.util.*;  
  3.   
  4. //影像檔案類  
  5. class ImageFile {  
  6.     private String name;  
  7.   
  8.     public ImageFile(String name) {  
  9.         this.name = name;  
  10.     }  
  11.   
  12.     public void killVirus() {  
  13.         //簡化程式碼,模擬防毒  
  14.         System.out.println("----對影像檔案'" + name + "'進行防毒");  
  15.     }  
  16. }  
  17.   
  18. //文字檔案類  
  19. class TextFile {  
  20.     private String name;  
  21.   
  22.     public TextFile(String name) {  
  23.         this.name = name;  
  24.     }  
  25.   
  26.     public void killVirus() {  
  27.         //簡化程式碼,模擬防毒  
  28.         System.out.println("----對文字檔案'" + name + "'進行防毒");  
  29.     }  
  30. }  
  31.   
  32. //資料夾類  
  33. class Folder {  
  34.     private String name;  
  35.     //定義集合folderList,用於儲存Folder型別的成員  
  36.     private ArrayList<Folder> folderList = new ArrayList<Folder>();  
  37.     //定義集合imageList,用於儲存ImageFile型別的成員  
  38.     private ArrayList<ImageFile> imageList = new ArrayList<ImageFile>();  
  39.     //定義集合textList,用於儲存TextFile型別的成員  
  40.     private ArrayList<TextFile> textList = new ArrayList<TextFile>();  
  41.       
  42.     public Folder(String name) {  
  43.         this.name = name;  
  44.     }  
  45.       
  46.     //增加新的Folder型別的成員  
  47.     public void addFolder(Folder f) {  
  48.         folderList.add(f);  
  49.     }  
  50.       
  51.     //增加新的ImageFile型別的成員  
  52.     public void addImageFile(ImageFile image) {  
  53.         imageList.add(image);  
  54.     }  
  55.       
  56.     //增加新的TextFile型別的成員  
  57.     public void addTextFile(TextFile text) {  
  58.         textList.add(text);  
  59.     }  
  60.           
  61.     //需提供三個不同的方法removeFolder()、removeImageFile()和removeTextFile()來刪除成員,程式碼省略  
  62.   
  63.     //需提供三個不同的方法getChildFolder(int i)、getChildImageFile(int i)和getChildTextFile(int i)來獲取成員,程式碼省略  
  64.   
  65.     public void killVirus() {  
  66.         System.out.println("****對資料夾'" + name + "'進行防毒");  //模擬防毒  
  67.           
  68.         //如果是Folder型別的成員,遞迴呼叫Folder的killVirus()方法  
  69.         for(Object obj : folderList) {  
  70.             ((Folder)obj).killVirus();  
  71.         }  
  72.           
  73.         //如果是ImageFile型別的成員,呼叫ImageFile的killVirus()方法  
  74.         for(Object obj : imageList) {  
  75.             ((ImageFile)obj).killVirus();  
  76.         }  
  77.           
  78.         //如果是TextFile型別的成員,呼叫TextFile的killVirus()方法  
  79.         for(Object obj : textList) {  
  80.             ((TextFile)obj).killVirus();  
  81.         }  
  82.     }   
  83. }  

      編寫如下客戶端測試程式碼進行測試:

[java] view plaincopy
  1. class Client {  
  2.     public static void main(String args[]) {  
  3.         Folder folder1,folder2,folder3;  
  4.         folder1 = new Folder("Sunny的資料");  
  5.         folder2 = new Folder("影像檔案");  
  6.         folder3 = new Folder("文字檔案");  
  7.           
  8.         ImageFile image1,image2;  
  9.         image1 = new ImageFile("小龍女.jpg");  
  10.         image2 = new ImageFile("張無忌.gif");  
  11.           
  12.         TextFile text1,text2;  
  13.         text1 = new TextFile("九陰真經.txt");  
  14.         text2 = new TextFile("葵花寶典.doc");  
  15.           
  16.         folder2.addImageFile(image1);  
  17.         folder2.addImageFile(image2);  
  18.         folder3.addTextFile(text1);  
  19.         folder3.addTextFile(text2);  
  20.         folder1.addFolder(folder2);  
  21.         folder1.addFolder(folder3);  
  22.           
  23.         folder1.killVirus();  
  24.     }  
  25. }  

      編譯並執行程式,輸出結果如下:

****對資料夾'Sunny的資料'進行防毒

****對資料夾'影像檔案'進行防毒

----對影像檔案'小龍女.jpg'進行防毒

----對影像檔案'張無忌.gif'進行防毒

****對資料夾'文字檔案'進行防毒

----對文字檔案'九陰真經.txt'進行防毒

----對文字檔案'葵花寶典.doc'進行防毒

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

      (1) 資料夾類Folder的設計和實現都非常複雜,需要定義多個集合儲存不同型別的成員,而且需要針對不同的成員提供增加、刪除和獲取等管理和訪問成員的方法,存在大量的冗餘程式碼,系統維護較為困難;

      (2) 由於系統沒有提供抽象層,客戶端程式碼必須有區別地對待充當容器的資料夾Folder和充當葉子的ImageFileTextFile,無法統一對它們進行處理;

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

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


2 組合模式概述

      對於樹形結構,當容器物件(如資料夾)的某一個方法被呼叫時,將遍歷整個樹形結構,尋找也包含這個方法的成員物件(可以是容器物件,也可以是葉子物件)並呼叫執行,牽一而動百,其中使用了遞迴呼叫的機制來對整個結構進行處理。由於容器物件和葉子物件在功能上的區別,在使用這些物件的程式碼中必須有區別地對待容器物件和葉子物件,而實際上大多數情況下我們希望一致地處理它們,因為對於這些物件的區別對待將會使得程式非常複雜。組合模式為解決此類問題而誕生,它可以讓葉子物件和容器物件的使用具有一致性。

      組合模式定義如下:

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

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

3  組合模式結構圖

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

       Component(抽象構件):它可以是介面或抽象類,為葉子構件和容器構件物件宣告介面,在該角色中可以包含所有子類共有行為的宣告和實現。在抽象構件中定義了訪問及管理它的子構件的方法,如增加子構件、刪除子構件、獲取子構件等。

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

       Composite(容器構件):它在組合結構中表示容器節點物件,容器節點包含子節點,其子節點可以是葉子節點,也可以是容器節點,它提供一個集合用於儲存子節點,實現了在抽象構件中定義的行為,包括那些訪問及管理子構件的方法,在其業務方法中可以遞迴呼叫其子節點的業務方法。

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

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

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

[java] view plaincopy
  1. abstract class Component {  
  2.     public abstract void add(Component c); //增加成員  
  3.     public abstract void remove(Component c); //刪除成員  
  4.     public abstract Component getChild(int i); //獲取成員  
  5.     public abstract void operation();  //業務方法  
  6. }  

      一般將抽象構件類設計為介面或抽象類,將所有子類共有方法的宣告和實現放在抽象構件類中。對於客戶端而言,將針對抽象構件程式設計,而無須關心其具體子類是容器構件還是葉子構件。

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

[java] view plaincopy
  1. class Leaf extends Component {  
  2.     public void add(Component c) {   
  3.         //異常處理或錯誤提示   
  4.     }     
  5.           
  6.     public void remove(Component c) {   
  7.         //異常處理或錯誤提示   
  8.     }  
  9.       
  10.     public Component getChild(int i) {   
  11.         //異常處理或錯誤提示  
  12.         return null;   
  13.     }  
  14.       
  15.     public void operation() {  
  16.         //葉子構件具體業務方法的實現  
  17.     }   
  18. }  

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

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

[java] view plaincopy
  1. class Composite extends Component {  
  2.     private ArrayList<Component> list = new ArrayList<Component>();  
  3.       
  4.     public void add(Component c) {  
  5.         list.add(c);  
  6.     }  
  7.       
  8.     public void remove(Component c) {  
  9.         list.remove(c);  
  10.     }  
  11.       
  12.     public Component getChild(int i) {  
  13.         return (Component)list.get(i);  
  14.     }  
  15.       
  16.     public void operation() {  
  17.         //容器構件具體業務方法的實現  
  18.         //遞迴呼叫成員構件的業務方法  
  19.         for(Object obj:list) {  
  20.             ((Component)obj).operation();  
  21.         }  
  22.     }     
  23. }  

      在容器構件中實現了在抽象構件中宣告的所有方法,既包括業務方法,也包括用於訪問和管理成員子構件的方法,如add()remove()getChild()等方法。需要注意的是在實現具體業務方法時,由於容器構件充當的是容器角色,包含成員構件,因此它將呼叫其成員構件的業務方法。在組合模式結構中,由於容器構件中仍然可以包含容器構件,因此在對容器構件進行處理時需要使用遞迴演算法,即在容器構件的operation()方法中遞迴呼叫其成員構件的operation()方法。

思考

      在組合模式結構圖中,如果聚合關聯關係不是從CompositeComponent的,而是從CompositeLeaf的,如圖4所示,會產生怎樣的結果?

4   組合模式思考題結構圖



3  完整解決方案

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

5  防毒軟體框架設計結構圖

    在圖5中, AbstractFile充當抽象構件類,Folder充當容器構件類,ImageFileTextFileVideoFile充當葉子構件類。完整程式碼如下所示:

[java] view plaincopy
  1. import java.util.*;  
  2.   
  3. //抽象檔案類:抽象構件  
  4. abstract class AbstractFile {  
  5.     public abstract void add(AbstractFile file);  
  6.     public abstract void remove(AbstractFile file);  
  7.     public abstract AbstractFile getChild(int i);  
  8.     public abstract void killVirus();  
  9. }  
  10.   
  11. //影像檔案類:葉子構件  
  12. class ImageFile extends AbstractFile {  
  13.     private String name;  
  14.       
  15.     public ImageFile(String name) {  
  16.         this.name = name;  
  17.     }  
  18.       
  19.     public void add(AbstractFile file) {  
  20.        System.out.println("對不起,不支援該方法!");  
  21.     }  
  22.       
  23.     public void remove(AbstractFile file) {  
  24.         System.out.println("對不起,不支援該方法!");  
  25.     }  
  26.       
  27.     public AbstractFile getChild(int i) {  
  28.         System.out.println("對不起,不支援該方法!");  
  29.         return null;  
  30.     }  
  31.       
  32.     public void killVirus() {  
  33.         //模擬防毒  
  34.         System.out.println("----對影像檔案'" + name + "'進行防毒");  
  35.     }  
  36. }  
  37.   
  38. //文字檔案類:葉子構件  
  39. class TextFile extends AbstractFile {  
  40.     private String name;  
  41.       
  42.     public TextFile(String name) {  
  43.         this.name = name;  
  44.     }  
  45.       
  46.     public void add(AbstractFile file) {  
  47.        System.out.println("對不起,不支援該方法!");  
  48.     }  
  49.       
  50.     public void remove(AbstractFile file) {  
  51.         System.out.println("對不起,不支援該方法!");  
  52.     }  
  53.       
  54.     public AbstractFile getChild(int i) {  
  55.         System.out.println("對不起,不支援該方法!");  
  56.         return null;  
  57.     }  
  58.       
  59.     public void killVirus() {  
  60.         //模擬防毒  
  61.         System.out.println("----對文字檔案'" + name + "'進行防毒");  
  62.     }  
  63. }  
  64.   
  65. //視訊檔案類:葉子構件  
  66. class VideoFile extends AbstractFile {  
  67.     private String name;  
  68.       
  69.     public VideoFile(String name) {  
  70.         this.name = name;  
  71.     }  
  72.       
  73.     public void add(AbstractFile file) {  
  74.        System.out.println("對不起,不支援該方法!");  
  75.     }  
  76.       
  77.     public void remove(AbstractFile file) {  
  78.         System.out.println("對不起,不支援該方法!");  
  79.     }  
  80.       
  81.     public AbstractFile getChild(int i) {  
  82.         System.out.println("對不起,不支援該方法!");  
  83.         return null;  
  84.     }  
  85.       
  86.     public void killVirus() {  
  87.         //模擬防毒  
  88.         System.out.println("----對視訊檔案'" + name + "'進行防毒");  
  89.     }  
  90. }  
  91.   
  92. //資料夾類:容器構件  
  93. class Folder extends AbstractFile {  
  94.     //定義集合fileList,用於儲存AbstractFile型別的成員  
  95.     private ArrayList<AbstractFile> fileList=new ArrayList<AbstractFile>();  
  96.     private String name;  
  97.           
  98.     public Folder(String name) {  
  99.         this.name = name;  
  100.     }  
  101.       
  102.     public void add(AbstractFile file) {  
  103.        fileList.add(file);    
  104.     }  
  105.       
  106.     public void remove(AbstractFile file) {  
  107.         fileList.remove(file);  
  108.     }  
  109.       
  110.     public AbstractFile getChild(int i) {  
  111.         return (AbstractFile)fileList.get(i);  
  112.     }  
  113.       
  114.     public void killVirus() {  
  115.         System.out.println("****對資料夾'" + name + "'進行防毒");  //模擬防毒  
  116.           
  117.         //遞迴呼叫成員構件的killVirus()方法  
  118.         for(Object obj : fileList) {  
  119.             ((AbstractFile)obj).killVirus();  
  120.         }  
  121.     }  
  122. }  

      編寫如下客戶端測試程式碼:

[java] view plaincopy
  1. class Client {  
  2.     public static void main(String args[]) {  
  3.         //針對抽象構件程式設計  
  4.         AbstractFile file1,file2,file3,file4,file5,folder1,folder2,folder3,folder4;  
  5.           
  6.         folder1 = new Folder("Sunny的資料");  
  7.         folder2 = new Folder("影像檔案");  
  8.         folder3 = new Folder("文字檔案");  
  9.         folder4 = new Folder("視訊檔案");  
  10.           
  11.         file1 = new ImageFile("小龍女.jpg");  
  12.         file2 = new ImageFile("張無忌.gif");  
  13.         file3 = new TextFile("九陰真經.txt");  
  14.         file4 = new TextFile("葵花寶典.doc");  
  15.         file5 = new VideoFile("笑傲江湖.rmvb");  
  16.   
  17.         folder2.add(file1);  
  18.         folder2.add(file2);  
  19.         folder3.add(file3);  
  20.         folder3.add(file4);  
  21.         folder4.add(file5);  
  22.         folder1.add(folder2);  
  23.         folder1.add(folder3);  
  24.         folder1.add(folder4);  
  25.           
  26.         //從“Sunny的資料”節點開始進行防毒操作  
  27.         folder1.killVirus();  
  28.     }  
  29. }  

      編譯並執行程式,輸出結果如下:

****對資料夾'Sunny的資料'進行防毒

****對資料夾'影像檔案'進行防毒

----對影像檔案'小龍女.jpg'進行防毒

----對影像檔案'張無忌.gif'進行防毒

****對資料夾'文字檔案'進行防毒

----對文字檔案'九陰真經.txt'進行防毒

----對文字檔案'葵花寶典.doc'進行防毒

****對資料夾'視訊檔案'進行防毒

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

      由於在本例項中使用了組合模式,在抽象構件類中宣告瞭所有方法,包括用於管理和訪問子構件的方法,如add()方法和remove()方法等,因此在ImageFile等葉子構件類中實現這些方法時必須進行相應的異常處理或錯誤提示。在容器構件類FolderkillVirus()方法中將遞迴呼叫其成員物件的killVirus()方法,從而實現對整個樹形結構的遍歷。

      如果需要更換操作節點,例如只需對資料夾“文字檔案”進行防毒,客戶端程式碼只需修改一行即可,將程式碼:

folder1.killVirus();

       改為:

folder3.killVirus();

       輸出結果如下:

****對資料夾'文字檔案'進行防毒

----對文字檔案'九陰真經.txt'進行防毒

----對文字檔案'葵花寶典.doc'進行防毒

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


4  透明組合模式與安全組合模式

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

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

[java] view plaincopy
  1. //提供預設實現的抽象構件類  
  2. abstract class AbstractFile {  
  3.     public void add(AbstractFile file) {  
  4.         System.out.println("對不起,不支援該方法!");  
  5.     }  
  6.       
  7.     public void remove(AbstractFile file) {  
  8.         System.out.println("對不起,不支援該方法!");  
  9.     }  
  10.       
  11.     public AbstractFile getChild(int i) {  
  12.         System.out.println("對不起,不支援該方法!");  
  13.         return null;  
  14.     }  
  15.       
  16.     public abstract void killVirus();  
  17. }  

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

[java] view plaincopy
  1. class Client {  
  2.     public static void main(String args[]) {  
  3.         //不能透明處理葉子構件  
  4.         ImageFile file1,file2;  
  5.         TextFile file3,file4;  
  6.         VideoFile file5;  
  7.         AbstractFile folder1,folder2,folder3,folder4;  
  8.         //其他程式碼省略  
  9.       }  
  10. }  

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

      解決方案二:除此之外,還有一種解決方法是在抽象構件AbstractFile中不宣告任何用於訪問和管理成員構件的方法,程式碼如下所示:

[java] view plaincopy
  1. abstract class AbstractFile {  
  2.     public abstract void killVirus();  
  3. }  

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

[java] view plaincopy
  1. class Client {  
  2.     public static void main(String args[]) {  
  3.           
  4.         AbstractFile file1,file2,file3,file4,file5;  
  5.         Folder folder1,folder2,folder3,folder4; //不能透明處理容器構件  
  6.         //其他程式碼省略  
  7.     }  
  8. }  

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

      (1) 透明組合模式

      透明組合模式中,抽象構件Component中宣告瞭所有用於管理成員物件的方法,包括add()remove()以及getChild()等方法,這樣做的好處是確保所有的構件類都有相同的介面。在客戶端看來,葉子物件與容器物件所提供的方法是一致的,客戶端可以相同地對待所有的物件。透明組合模式也是組合模式的標準形式,雖然上面的解決方案一在客戶端可以有不透明的實現方法,但是由於在抽象構件中包含add()remove()等方法,因此它還是透明組合模式,透明組合模式的完整結構如圖6所示:

6  透明組合模式結構圖

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

      (2) 安全組合模式

      安全組合模式中,在抽象構件Component中沒有宣告任何用於管理成員物件的方法,而是在Composite類中宣告並實現這些方法。這種做法是安全的,因為根本不向葉子物件提供這些管理成員物件的方法,對於葉子物件,客戶端不可能呼叫到這些方法,這就是解決方案二所採用的實現方式。安全組合模式的結構如圖10-7所示:

7  安全組合模式結構圖

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


5 公司組織結構

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

8  Sunny公司組織結構圖

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

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

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


6 組合模式總結

      組合模式使用物件導向的思想來實現樹形結構的構建與處理,描述瞭如何將容器物件和葉子物件進行遞迴組合,實現簡單,靈活性好。由於在軟體開發中存在大量的樹形結構,因此組合模式是一種使用頻率較高的結構型設計模式,Java SE中的AWTSwing包的設計就基於組合模式,在這些介面包中為使用者提供了大量的容器構件(如Container)和成員構件(如CheckboxButtonTextComponent等),其結構如圖10所示:

10 AWT組合模式結構示意圖

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

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

      1. 主要優點

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

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

      (2) 客戶端可以一致地使用一個組合結構或其中單個物件,不必關心處理的是單個物件還是整個組合結構,簡化了客戶端程式碼。

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

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

      2. 主要缺點

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

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

      3. 適用場景

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

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

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

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

 

練習

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


相關文章