設計模式--組合模式

目錄

本文的結構如下:

  • 引言
  • 什么是組合模式
  • 模式的結構
  • 典型代碼
  • 代碼示例
  • 優點和缺點
  • 適用環境
  • 模式應用

一、引言

樹形結構是很常見的,比如目錄系統,隨便點開一個文件夾,文件夾下面可能有文件,也有子文件夾,子文件夾中還有子子文件夾和文件......

20171127_composite01.png

還有導航中的菜單。

20171127_composite02.png

還有公司的部門構造等,展開來看都是樹形的結構。

這些樹形結構在面向對象的世界中一般是用組合模式來處理的。

組合模式通過一種巧妙的設計方案,可以一致性地處理整個樹形結構或者樹形結構的一部分,也可以一致性地處理樹形結構中的葉子節點(不包含子節點的節點)和容器節點(包含子節點的節點)。

二、什么是組合模式

對于樹形結構,當容器對象(如文件夾)的某一個方法被調用時,將遍歷整個樹形結構,尋找也包含這個方法的成員對象(可以是容器對象,也可以是葉子對象)并調用執行。這是靠遞歸調用的機制實現的。

由于容器對象和葉子對象在功能上的區別,在使用這些對象的代碼中必須有區別地對待容器對象和葉子對象,而實際上大多數情況下我們希望一致地處理它們,因為對于這些對象的區別對待將會使得程序非常復雜。組合模式為解決此類問題而誕生,它可以讓葉子對象和容器對象的使用具有一致性。

組合模式定義如下:

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

三、模式的結構

組合模式的UML類圖如下:

20171127_composite03.png

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

  • Component(抽象構件):它可以是接口或抽象類,為葉子構件和容器構件對象聲明接口,在該角色中可以包含所有子類共有行為的聲明和實現。在抽象構件中定義了訪問及管理它的子構件的方法,如增加子構件、刪除子構件、獲取子構件等。
  • Leaf(葉子構件):它在組合結構中表示葉子節點對象,葉子節點沒有子節點,它實現了在抽象構件中定義的行為。對于那些訪問及管理子構件的方法,可以通過異常等方式進行處理。
  • Composite(容器構件):它在組合結構中表示容器節點對象,容器節點包含子節點,其子節點可以是葉子節點,也可以是容器節點,它提供一個集合用于存儲子節點,實現了在抽象構件中定義的行為,包括那些訪問及管理子構件的方法,在其業務方法中可以遞歸調用其子節點的業務方法。

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

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

四、典型代碼

4.1、抽象構件角色

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

public abstract class Component {
    /**
     * 增加成員
     * @param c
     */
    public void add(Component c){
        throw new UnsupportedOperationException();
    }

    /**
     * 刪除成員
     * @param c
     */
    public void remove(Component c){
        throw new UnsupportedOperationException();
    }

    /**
     * 獲取成員
     * @param i
     * @return
     */
    public Component getChild(int i){
        throw new UnsupportedOperationException();
    }

    /**
     * 業務方法
     */
    public void operation(){
        throw new UnsupportedOperationException();
    }
}

為什么所有方法都拋出UnsupportedOperationException?是因為有些方法只對容器構件有意義,而有些方法只對葉子構件有意義,這樣,如果某個子構件不支持某個操作,直接繼承默認方法就可以了。

4.2、葉子構件

葉子構件繼承自抽象構建抽象構建:

public class Leaf extends Component {
    @Override
    public void operation(){
        System.out.println("子構件");
    }
}

葉子構件不能再包含子構件,因此在葉子構件中只需事先業務方法,其他默認繼承,拋出為什么所有方法都拋出UnsupportedOperationException。

4.3、容器構件

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

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

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

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

    @Override
    public void operation(){
        for (Component child: list){
            child.operation();
        }
    }
}

容器構件中實現了在抽象構件中聲明的所有方法,既包括業務方法,也包括用于訪問和管理成員子構件的方法。

需要注意的是在實現具體業務方法時,由于容器構件充當的是容器角色,包含成員構件,因此它將調用其成員構件的業務方法。在組合模式結構中,由于容器構件中仍然可以包含容器構件,因此在對容器構件進行處理時需要使用遞歸算法,即在容器構件的operation()方法中遞歸調用其成員構件的operation()方法。

五、代碼示例

假設這樣的場景:

在電腦E盤有個文件夾,該文件夾下面有很多文件,有視頻文件,有音頻文件,有圖像文件,還有包含視頻、音頻及圖像的文件夾,十分雜亂,現希望將這些雜亂的文件展示出來。

20171127_composite04.png

這里其實就是一個樹形結構,根據一定的規則分類后,大致是這樣的:

20171127_composite05.png

5.1、不使用組合模式

注:當然可以一個循環遍歷就搞定了,因為這里用的是文件的形式,如果是別的形式呢?所以不要太較真了,只是舉例。

public class MusicFile {
    private String name;

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

    public void print(){
        System.out.println(name);
    }
}

public class VideoFile {
    private String name;

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

    public void print(){
        System.out.println(name);
    }
}

public class ImageFile {
    private String name;

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

    public void print(){
        System.out.println(name);
    }
}

public class Folder {
    private String name;
    //音樂
    private List<MusicFile> musicList = new ArrayList<MusicFile>();
    //視頻
    private List<VideoFile> videoList = new ArrayList<VideoFile>();
    //圖片
    private List<ImageFile> imageList = new ArrayList<ImageFile>();
    //文件夾
    private List<Folder> folderList = new ArrayList<Folder>();

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

    public void addFolder(Folder folder){
        folderList.add(folder);
    }

    public void addImage(ImageFile image){
        imageList.add(image);
    }

    public void addVideo(VideoFile video){
        videoList.add(video);
    }

    public void addMusic(MusicFile music){
        musicList.add(music);
    }

    public void print(){
        for (MusicFile music : musicList){
            music.print();
        }
        for (VideoFile video : videoList){
            video.print();
        }
        for(ImageFile image : imageList){
            image.print();
        }
        for (Folder folder : folderList){
            folder.print();
        }
    }
}

客戶端測試:

public class Client {
    public static void main(String[] args) {
        MusicFile m1 = new MusicFile("盡頭.mp3");
        MusicFile m2 = new MusicFile("飄洋過海來看你.mp3");
        MusicFile m3 = new MusicFile("曾經的你.mp3");
        MusicFile m4 = new MusicFile("take me to your heart.mp3");

        VideoFile v1 = new VideoFile("戰狼2.mp4");
        VideoFile v2 = new VideoFile("理想.avi");
        VideoFile v3 = new VideoFile("瑯琊榜.avi");

        ImageFile i1 = new ImageFile("敦煌.png");
        ImageFile i2 = new ImageFile("baby.jpg");
        ImageFile i3 = new ImageFile("girl.jpg");

        Folder aa = new Folder("aa");
        aa.addImage(i3);

        Folder bb = new Folder("bb");
        bb.addMusic(m4);
        bb.addVideo(v3);

        Folder top = new Folder("top");
        top.addFolder(aa);
        top.addFolder(bb);
        top.addMusic(m1);
        top.addMusic(m2);
        top.addMusic(m3);
        top.addVideo(v1);
        top.addVideo(v2);
        top.addImage(i1);
        top.addImage(i2);

        top.print();
    }
}

如果采用上述的形式,有幾個缺點:

  • 文件夾類Folder的設計和實現都非常復雜,需要定義多個集合存儲不同類型的成員,而且需要針對不同的成員提供增加、刪除和獲取等管理和訪問成員的方法,存在大量的冗余代碼,系統維護較為困難;
  • 由于系統沒有提供抽象層,客戶端代碼必須有區別地對待充當容器的文件夾Folder和充當葉子的MusicFile、ImageFile和VideoFile,無法統一對它們進行處理;
  • 系統的靈活性和可擴展性差,如果增加了新的類型的葉子和容器都需要對原有代碼進行修改,例如如果需要在系統中增加一種新類型的文本文件TextFile,則必須修改Folder類的源代碼,否則無法在文件夾中添加文本文件。

5.2、使用組合模式改進

為了讓系統具有更好的靈活性和可擴展性,客戶端可以一致地對待文件和文件夾,定義一個抽象構件AbstractFile,Folder充當容器構件,MusicFile、VideoFile和ImageFile充當葉子構件。

20171127_composite06.png

抽象構件AbstractFile:

public abstract class AbstractFile {
    public void add(AbstractFile file){
        throw new UnsupportedOperationException();
    }
    
    public void remove(AbstractFile file){
        throw new UnsupportedOperationException();
    }

    public AbstractFile getChild(int i){
        throw new UnsupportedOperationException();
    }
    
    public void print(){
        throw new UnsupportedOperationException();
    }
}

葉子構件:

public class MusicFile extends AbstractFile{
    private String name;

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

    public void print(){
        System.out.println(name);
    }
}

public class VideoFile extends AbstractFile{
    private String name;

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

    public void print(){
        System.out.println(name);
    }
}

public class ImageFile extends AbstractFile{
    private String name;

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

    public void print(){
        System.out.println(name);
    }
}

容器構件:

public class Folder extends AbstractFile{
    private String name;
    private List<AbstractFile> files = new ArrayList<AbstractFile>();

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

    @Override
    public void add(AbstractFile file){
        files.add(file);
    }

    @Override
    public void remove(AbstractFile file){
        files.remove(file);
    }

    @Override
    public AbstractFile getChild(int i){
        return files.get(i);
    }

    @Override
    public void print(){
        for (AbstractFile file : files){
            file.print();
        }
    }
}

客戶端測試:

public class Client {
    public static void main(String[] args) {
        AbstractFile m1 = new MusicFile("盡頭.mp3");
        AbstractFile m2 = new MusicFile("飄洋過海來看你.mp3");
        AbstractFile m3 = new MusicFile("曾經的你.mp3");
        AbstractFile m4 = new MusicFile("take me to your heart.mp3");

        AbstractFile v1 = new VideoFile("戰狼2.mp4");
        AbstractFile v2 = new VideoFile("理想.avi");
        AbstractFile v3 = new VideoFile("瑯琊榜.avi");

        AbstractFile i1 = new ImageFile("敦煌.png");
        AbstractFile i2 = new ImageFile("baby.jpg");
        AbstractFile i3 = new ImageFile("girl.jpg");

        AbstractFile aa = new Folder("aa");
        aa.add(i3);

        AbstractFile bb = new Folder("bb");
        bb.add(m4);
        bb.add(v3);

        AbstractFile top = new Folder("top");
        top.add(aa);
        top.add(bb);
        top.add(m1);
        top.add(m2);
        top.add(m3);
        top.add(v1);
        top.add(v2);
        top.add(i1);
        top.add(i2);

        top.print();
    }
}

用組合模式提供一個抽象構件后,客戶端可以一致對待容器構件和葉子構件,進行統一處理,并且大量減少了冗余,擴展性也很好,新增TextFile無需修改Folder源碼,只需修改客戶端即可。

當然,這里似乎有點違法“迭代器模式”中講的“單一職責原則”,的確是,抽象構件不但要管理層次結構,還要執行一些業務操作。

我覺得應該這樣理解問題,設計模式并不應該是生套似的,各種設計原則也并不是說一定不能破壞的,所有的種種都是為了更好的解決問題,更好的進行擴展維護,當適度破壞既定的原則,卻可以更好的解決問題時,顯然這里以單一設計原則換取了透明性,這種折中方案是可取的。

六、優點和缺點

6.1、優點

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

  • 組合模式可以清楚地定義分層次的復雜對象,表示對象的全部或部分層次,它讓客戶端忽略了層次的差異,方便對整個層次結構進行控制。
  • 客戶端可以一致地使用一個組合結構或其中單個對象,不必關心處理的是單個對象還是整個組合結構,簡化了客戶端代碼。
  • 在組合模式中增加新的容器構件和葉子構件都很方便,無須對現有類庫進行任何修改,符合“開閉原則”。
  • 組合模式為樹形結構的面向對象實現提供了一種靈活的解決方案,通過葉子對象和容器對象的遞歸組合,可以形成復雜的樹形結構,但對樹形結構的控制卻非常簡單。

6.2、缺點

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

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

七、適用環境

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

  • 在具有整體和部分的層次結構中,希望通過一種方式忽略整體與部分的差異,客戶端可以一致地對待它們。
  • 在一個使用面向對象語言開發的系統中需要處理一個樹形結構。
  • 在一個系統中能夠分離出葉子對象和容器對象,而且它們的類型不固定,需要增加一些新的類型。

八、模式應用

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

20171127_composite07.png
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 繼承是is-a的關系。組合和聚合有點像,有些書上沒有作區分,都稱之為has-a,有些書上對其進行了較為嚴格區分,組...
    時待吾閱讀 475評論 0 1
  • 介紹 這篇主要講述設計模式中的組合模式。組合模式又叫部分整體模式,是用于把一組相似的對象當作一個單一的對象。組合模...
    東西的南北閱讀 344評論 0 1
  • 1.組合模式的定義及使用場景組合模式也稱為部分整體模式,結構型設計模式之一,組合模式比較簡單,它將一組相似的對象看...
    GB_speak閱讀 873評論 0 2
  • View 和 ViewGroup 的 關系 在我們前面對 事件的分發 和 View 的分發中我們可以知道這兩者是密...
    銳_nmpoi閱讀 2,034評論 0 0
  • 原文地址:LoveDev 對于樹形結構,容器對象(如文件夾)可以進行添加刪除葉子對象(如文件)等操作,但是葉子對象...
    KevinLive閱讀 389評論 2 1