結構型SEQ3 - 組合模式 Composite Pattern

【學習難度:★★★☆☆,使用頻率:★★★★☆】
直接出處:組合模式
梳理和學習:https://github.com/BruceOuyang/boy-design-pattern
簡書日期: 2018/03/12
簡書首頁:http://www.lxweimin.com/p/0fb891a7c5ed

樹形結構的處理——組合模式(一)

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

11.1 設計殺毒軟件的框架結構

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:

//為了突出核心框架代碼,我們對殺毒過程的實現進行了大量簡化 
import java.util.*; 

//圖像文件類 
class ImageFile { 
private String name; 

public ImageFile(String name) { 
this.name = name; 
} 

public void killVirus() { 
//簡化代碼,模擬殺毒 
System.out.println("----對圖像文件'" + name + "'進行殺毒"); 
} 
} 

//文本文件類 
class TextFile { 
private String name; 

public TextFile(String name) { 
this.name = name; 
} 

public void killVirus() { 
//簡化代碼,模擬殺毒 
System.out.println("----對文本文件'" + name + "'進行殺毒"); 
} 
} 

//文件夾類 
class Folder { 
private String name; 
//定義集合folderList,用于存儲Folder類型的成員 
private ArrayList<Folder> folderList = new ArrayList<Folder>(); 
//定義集合imageList,用于存儲ImageFile類型的成員 
private ArrayList<ImageFile> imageList = new ArrayList<ImageFile>(); 
//定義集合textList,用于存儲TextFile類型的成員 
private ArrayList<TextFile> textList = new ArrayList<TextFile>(); 

public Folder(String name) { 
this.name = name; 
} 

//增加新的Folder類型的成員 
public void addFolder(Folder f) { 
folderList.add(f); 
} 

//增加新的ImageFile類型的成員 
public void addImageFile(ImageFile image) { 
imageList.add(image); 
} 

//增加新的TextFile類型的成員 
public void addTextFile(TextFile text) { 
textList.add(text); 
} 

//需提供三個不同的方法removeFolder()、removeImageFile()和removeTextFile()來刪除成員,代碼省略 

//需提供三個不同的方法getChildFolder(int i)、getChildImageFile(int i)和getChildTextFile(int i)來獲取成員,代碼省略 

public void killVirus() { 
System.out.println("****對文件夾'" + name + "'進行殺毒"); //模擬殺毒 

//如果是Folder類型的成員,遞歸調用Folder的killVirus()方法 
for(Object obj : folderList) { 
((Folder)obj).killVirus(); 
} 

//如果是ImageFile類型的成員,調用ImageFile的killVirus()方法 
for(Object obj : imageList) { 
((ImageFile)obj).killVirus(); 
} 

//如果是TextFile類型的成員,調用TextFile的killVirus()方法 
for(Object obj : textList) { 
((TextFile)obj).killVirus(); 
} 
} 
}

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

class Client { 
public static void main(String args[]) { 
Folder folder1,folder2,folder3; 
folder1 = new Folder("Sunny的資料"); 
folder2 = new Folder("圖像文件"); 
folder3 = new Folder("文本文件"); 

ImageFile image1,image2; 
image1 = new ImageFile("小龍女.jpg"); 
image2 = new ImageFile("張無忌.gif"); 

TextFile text1,text2; 
text1 = new TextFile("九陰真經.txt"); 
text2 = new TextFile("葵花寶典.doc"); 

folder2.addImageFile(image1); 
folder2.addImageFile(image2); 
folder3.addTextFile(text1); 
folder3.addTextFile(text2); 
folder1.addFolder(folder2); 
folder1.addFolder(folder3); 

folder1.killVirus(); 
} 
}

編譯并運行程序,輸出結果如下:

****對文件夾'Sunny的資料'進行殺毒
****對文件夾'圖像文件'進行殺毒
----對圖像文件'小龍女.jpg'進行殺毒
----對圖像文件'張無忌.gif'進行殺毒
****對文件夾'文本文件'進行殺毒
----對文本文件'九陰真經.txt'進行殺毒
----對文本文件'葵花寶典.doc'進行殺毒

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

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

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

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

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

樹形結構的處理——組合模式(二)

11.2 組合模式概述

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

組合模式定義如下:

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

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

圖11-3 組合模式結構圖

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

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

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

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

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

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

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

abstract class Component {  
    public abstract void add(Component c); //增加成員  
    public abstract void remove(Component c); //刪除成員  
    public abstract Component getChild(int i); //獲取成員  
    public abstract void operation();  //業務方法  
}

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

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

class Leaf extends Component {  
    public void add(Component c) {   
        //異常處理或錯誤提示   
    }     

    public void remove(Component c) {   
        //異常處理或錯誤提示   
    }  

    public Component getChild(int i) {   
        //異常處理或錯誤提示  
        return null;   
    }  

    public void operation() {  
        //葉子構件具體業務方法的實現  
    }   
}

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

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

class Composite extends Component {  
    private ArrayList<Component> list = new ArrayList<Component>();  

    public void add(Component c) {  
        list.add(c);  
    }  

    public void remove(Component c) {  
        list.remove(c);  
    }  

    public Component getChild(int i) {  
        return (Component)list.get(i);  
    }  

    public void operation() {  
        //容器構件具體業務方法的實現  
        //遞歸調用成員構件的業務方法  
        for(Object obj:list) {  
            ((Component)obj).operation();  
        }  
    }     
}

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

思考

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

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

樹形結構的處理——組合模式(三)

11.3 完整解決方案

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

圖11-5 殺毒軟件框架設計結構圖

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

import java.util.*;  

//抽象文件類:抽象構件  
abstract class AbstractFile {  
    public abstract void add(AbstractFile file);  
    public abstract void remove(AbstractFile file);  
    public abstract AbstractFile getChild(int i);  
    public abstract void killVirus();  
}  

//圖像文件類:葉子構件  
class ImageFile extends AbstractFile {  
    private String name;  

    public ImageFile(String name) {  
        this.name = name;  
    }  

    public void add(AbstractFile file) {  
       System.out.println("對不起,不支持該方法!");  
    }  

    public void remove(AbstractFile file) {  
        System.out.println("對不起,不支持該方法!");  
    }  

    public AbstractFile getChild(int i) {  
        System.out.println("對不起,不支持該方法!");  
        return null;  
    }  

    public void killVirus() {  
        //模擬殺毒  
        System.out.println("----對圖像文件'" + name + "'進行殺毒");  
    }  
}  

//文本文件類:葉子構件  
class TextFile extends AbstractFile {  
    private String name;  

    public TextFile(String name) {  
        this.name = name;  
    }  

    public void add(AbstractFile file) {  
       System.out.println("對不起,不支持該方法!");  
    }  

    public void remove(AbstractFile file) {  
        System.out.println("對不起,不支持該方法!");  
    }  

    public AbstractFile getChild(int i) {  
        System.out.println("對不起,不支持該方法!");  
        return null;  
    }  

    public void killVirus() {  
        //模擬殺毒  
        System.out.println("----對文本文件'" + name + "'進行殺毒");  
    }  
}  

//視頻文件類:葉子構件  
class VideoFile extends AbstractFile {  
    private String name;  

    public VideoFile(String name) {  
        this.name = name;  
    }  

    public void add(AbstractFile file) {  
       System.out.println("對不起,不支持該方法!");  
    }  

    public void remove(AbstractFile file) {  
        System.out.println("對不起,不支持該方法!");  
    }  

    public AbstractFile getChild(int i) {  
        System.out.println("對不起,不支持該方法!");  
        return null;  
    }  

    public void killVirus() {  
        //模擬殺毒  
        System.out.println("----對視頻文件'" + name + "'進行殺毒");  
    }  
}  

//文件夾類:容器構件  
class Folder extends AbstractFile {  
    //定義集合fileList,用于存儲AbstractFile類型的成員  
    private ArrayList<AbstractFile> fileList=new ArrayList<AbstractFile>();  
    private String name;  

    public Folder(String name) {  
        this.name = name;  
    }  

    public void add(AbstractFile file) {  
       fileList.add(file);    
    }  

    public void remove(AbstractFile file) {  
        fileList.remove(file);  
    }  

    public AbstractFile getChild(int i) {  
        return (AbstractFile)fileList.get(i);  
    }  

    public void killVirus() {  
        System.out.println("****對文件夾'" + name + "'進行殺毒");  //模擬殺毒  

        //遞歸調用成員構件的killVirus()方法  
        for(Object obj : fileList) {  
            ((AbstractFile)obj).killVirus();  
        }  
    }  
}

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

class Client {  
    public static void main(String args[]) {  
        //針對抽象構件編程  
        AbstractFile file1,file2,file3,file4,file5,folder1,folder2,folder3,folder4;  

        folder1 = new Folder("Sunny的資料");  
        folder2 = new Folder("圖像文件");  
        folder3 = new Folder("文本文件");  
        folder4 = new Folder("視頻文件");  

        file1 = new ImageFile("小龍女.jpg");  
        file2 = new ImageFile("張無忌.gif");  
        file3 = new TextFile("九陰真經.txt");  
        file4 = new TextFile("葵花寶典.doc");  
        file5 = new VideoFile("笑傲江湖.rmvb");  

        folder2.add(file1);  
        folder2.add(file2);  
        folder3.add(file3);  
        folder3.add(file4);  
        folder4.add(file5);  
        folder1.add(folder2);  
        folder1.add(folder3);  
        folder1.add(folder4);  

        //從“Sunny的資料”節點開始進行殺毒操作  
        folder1.killVirus();  
    }  
}

編譯并運行程序,輸出結果如下:

****對文件夾'Sunny的資料'進行殺毒
****對文件夾'圖像文件'進行殺毒
----對圖像文件'小龍女.jpg'進行殺毒
----對圖像文件'張無忌.gif'進行殺毒
****對文件夾'文本文件'進行殺毒
----對文本文件'九陰真經.txt'進行殺毒
----對文本文件'葵花寶典.doc'進行殺毒
****對文件夾'視頻文件'進行殺毒
----對視頻文件'笑傲江湖.rmvb'進行殺毒

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

如果需要更換操作節點,例如只需對文件夾“文本文件”進行殺毒,客戶端代碼只需修改一行即可,將代碼:

folder1.killVirus();

改為:

folder3.killVirus();

輸出結果如下:

****對文件夾'文本文件'進行殺毒
----對文本文件'九陰真經.txt'進行殺毒
----對文本文件'葵花寶典.doc'進行殺毒

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

樹形結構的處理——組合模式(四)

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

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

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

//提供默認實現的抽象構件類  
abstract class AbstractFile {  
    public void add(AbstractFile file) {  
        System.out.println("對不起,不支持該方法!");  
    }  

    public void remove(AbstractFile file) {  
        System.out.println("對不起,不支持該方法!");  
    }  

    public AbstractFile getChild(int i) {  
        System.out.println("對不起,不支持該方法!");  
        return null;  
    }  

    public abstract void killVirus();  
}

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

class Client {  
    public static void main(String args[]) {  
        //不能透明處理葉子構件  
        ImageFile file1,file2;  
        TextFile file3,file4;  
        VideoFile file5;  
        AbstractFile folder1,folder2,folder3,folder4;  
        //其他代碼省略  
      }  
}

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

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

abstract class AbstractFile {  
    public abstract void killVirus();  
}

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

class Client {  
    public static void main(String args[]) {  

        AbstractFile file1,file2,file3,file4,file5;  
        Folder folder1,folder2,folder3,folder4; //不能透明處理容器構件  
        //其他代碼省略  
    }  
}

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

(1) 透明組合模式

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

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

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

(2) 安全組合模式

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

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

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

樹形結構的處理——組合模式(五)

11.5 公司組織結構

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

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

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

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

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

思考

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

11.6 組合模式總結

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

java-awt-swing-composite.jpg

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

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

  1. 主要優點

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

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

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

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

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

  1. 主要缺點

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

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

  1. 適用場景

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

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

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

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

練習

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

練習會在我的github上做掉

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內容

  • 目錄 本文的結構如下: 引言 什么是組合模式 模式的結構 典型代碼 代碼示例 優點和缺點 適用環境 模式應用 一、...
    w1992wishes閱讀 900評論 0 2
  • 1 場景問題# 1.1 商品類別樹## 考慮這樣一個實際的應用:管理商品類別樹。 在實現跟商品有關的應用系統的時候...
    七寸知架構閱讀 6,047評論 10 59
  • 設計模式匯總 一、基礎知識 1. 設計模式概述 定義:設計模式(Design Pattern)是一套被反復使用、多...
    MinoyJet閱讀 3,961評論 1 15
  • 組合多個對象形成樹形結構以表示具有“整體—部分”關系的層次結構。組合模式對單個對象(即葉子對象)和組合對象(即容器...
    lyu571閱讀 501評論 0 1
  • 在我七八歲的時候我媽就和我說過:“凡事要靠自己,自己不行,親戚都看不起你。”這個貌似是很有道理的一句話,起碼那...
    黑澤特日記閱讀 244評論 0 0