天天看點

設計模式(結構型)之組合模式(Composite Pattern)

PS一句:最終還是選擇CSDN來整理發表這幾年的知識點,該文章平行遷移到CSDN。因為CSDN也支援MarkDown文法了,牛逼啊!

【工匠若水 http://blog.csdn.net/yanbober】 閱讀前一篇《設計模式(結構型)之橋接模式(Bridge Pattern)》 http://blog.csdn.net/yanbober/article/details/45366781

概述

組合模式又叫做部分-整體模式,使我們在樹型結構的問題中模糊簡單元素和複雜元素的概念,客戶程式可以像處理簡單元素一樣來處理複雜的元素,進而使得客戶程式與複雜元素的内部結構解耦。組合模式可以優化處理遞歸或分級資料結構。有許多關于分級資料結構的例子,使得組合模式非常有用武之地。

設計模式(結構型)之組合模式(Composite Pattern)

核心

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

重點: 組合模式結構重要核心子產品:

抽象構件(Component)

組合中的對象聲明接口,在适當的情況下實作所有類共有接口的預設行為。聲明一個接口用于通路和管理Component子部件。

樹葉構件(Leaf)

在組合中表示樹的葉子結點對象,葉子結點沒有子結點。

容器構件(Composite)

定義有枝節點的部件,在Component接口中實作與子部件有關操作,如增加(add)和删除(remove)等。

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

使用場景

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

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

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

程式猿執行個體

假設問題環境:

就拿Android開發中最常見的一種情景來分析吧。我們有一種需求是周遊整個cache檔案夾下的所有檔案及檔案夾,列印出來(實際指定不是列印,而是邏輯操作,這裡示範而已)。如下我們先不使用組合模式。

package yanbober.github.io;

import java.util.ArrayList;
import java.util.List;

class MediaFile {
    private String mName;
    private String mDescription;

    public MediaFile(String mName, String mDescription) {
        this.mName = mName;
        this.mDescription = mDescription;
    }

    @Override
    public String toString() {
        return "MediaFile# Name="+mName+", Description="+mDescription;
    }
}

class OfficeFile {
    private String mName;
    private String mDescription;

    public OfficeFile(String mName, String mDescription) {
        this.mName = mName;
        this.mDescription = mDescription;
    }

    @Override
    public String toString() {
        return "OfficeFile# Name="+mName+", Description="+mDescription;
    }
}

class PackageFile {
    private String mName;
    private String mDescription;

    public PackageFile(String mName, String mDescription) {
        this.mName = mName;
        this.mDescription = mDescription;
    }

    @Override
    public String toString() {
        return "PackageFile# Name="+mName+", Description="+mDescription;
    }
}

class BaseFolder {
    private String mName;
    private String mDescription;

    private List<BaseFolder> mBaseFolderList = new ArrayList<>();
    private List<MediaFile> mMediaFileList = new ArrayList<>();
    private List<OfficeFile> mOfficeFileList = new ArrayList<>();
    private List<PackageFile> mPackageFileList = new ArrayList<>();

    public BaseFolder(String mName, String mDescription) {
        this.mName = mName;
        this.mDescription = mDescription;
    }

    public void addBaseFolder(BaseFolder baseFolder) {
        mBaseFolderList.add(baseFolder);
    }

    public void addMediaFile(MediaFile mediaFile) {
        mMediaFileList.add(mediaFile);
    }

    public void addOfficeFile(OfficeFile officeFile) {
        mOfficeFileList.add(officeFile);
    }

    public void addPackageFile(PackageFile packageFile) {
        mPackageFileList.add(packageFile);
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder("*****BaseFolder# enter folder is:"+mName+"\n");

        for (BaseFolder baseFolder : mBaseFolderList) {
            builder.append(baseFolder.toString()+"\n");
        }

        for (MediaFile mediaFile : mMediaFileList) {
            builder.append(mediaFile.toString()+"\n");
        }

        for (OfficeFile officeFile : mOfficeFileList) {
            builder.append(officeFile.toString()+"\n");
        }

        for (PackageFile packageFile : mPackageFileList) {
            builder.append(packageFile.toString()+"\n");
        }

        return builder.toString();
    }
}

public class Main {
    public static void main(String[] args) {
        BaseFolder folder = new BaseFolder("cache", "app cache dir.");
        MediaFile mediaFile = new MediaFile("test.png", "test picture.");
        folder.addMediaFile(mediaFile);
        mediaFile = new MediaFile("haha.mp4", "test video.");
        folder.addMediaFile(mediaFile);
        PackageFile packageFile = new PackageFile("yanbo.apk", "an android application.");
        folder.addPackageFile(packageFile);
        BaseFolder childDir = new BaseFolder("word", "office dir.");
        OfficeFile officeFile = new OfficeFile("rrrr.doc", "office doc file.");
        childDir.addOfficeFile(officeFile);
        folder.addBaseFolder(childDir);
        System.out.println(folder.toString());
    }
}
           

有了如上實作那麼問題來了。。。

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

準麼辦?升個級呗,用一下組合模式。

升個級吧

給上面代碼來個重構? 那就按照組合模式結構重要核心子產品來對上面代碼更新重構一把吧。

package yanbober.github.io;

import java.util.ArrayList;
import java.util.List;

abstract class AbstractComponent {
    public abstract void add(AbstractComponent c);
    public abstract AbstractComponent get(int index);
    public abstract String getString();
}

class MediaFile extends AbstractComponent {
    private String mName;
    private String mDescription;

    public MediaFile(String mName, String mDescription) {
        this.mName = mName;
        this.mDescription = mDescription;
    }

    @Override
    public void add(AbstractComponent c) {
        //unused
    }

    @Override
    public AbstractComponent get(int index) {
        return null;
    }

    @Override
    public String getString() {
        return "MediaFile# Name="+mName+", Description="+mDescription;
    }
}

class OfficeFile extends AbstractComponent {
    private String mName;
    private String mDescription;

    public OfficeFile(String mName, String mDescription) {
        this.mName = mName;
        this.mDescription = mDescription;
    }

    @Override
    public void add(AbstractComponent c) {
        //unused
    }

    @Override
    public AbstractComponent get(int index) {
        return null;
    }

    @Override
    public String getString() {
        return "OfficeFile# Name="+mName+", Description="+mDescription;
    }
}

class PackageFile extends AbstractComponent {
    private String mName;
    private String mDescription;

    public PackageFile(String mName, String mDescription) {
        this.mName = mName;
        this.mDescription = mDescription;
    }

    @Override
    public void add(AbstractComponent c) {
        //unused
    }

    @Override
    public AbstractComponent get(int index) {
        return null;
    }

    @Override
    public String getString() {
        return "PackageFile# Name="+mName+", Description="+mDescription;
    }
}

class BaseFolder extends AbstractComponent {
    private String mName;
    private String mDescription;

    private List<AbstractComponent> abstractComponents = new ArrayList<>();

    public BaseFolder(String mName, String mDescription) {
        this.mName = mName;
        this.mDescription = mDescription;
    }

    @Override
    public void add(AbstractComponent c) {
        abstractComponents.add(c);
    }

    @Override
    public AbstractComponent get(int index) {
        return abstractComponents.get(index);
    }

    @Override
    public String getString() {
        StringBuilder builder = new StringBuilder("*****BaseFolder# enter folder is:"+mName+"\n");

        for (AbstractComponent baseFolder : abstractComponents) {
            builder.append(baseFolder.getString()+"\n");
        }

        return builder.toString();
    }
}

public class Main {
    public static void main(String[] args) {
        AbstractComponent folder = new BaseFolder("cache", "app cache dir.");
        AbstractComponent mediaFile = new MediaFile("test.png", "test picture.");
        folder.add(mediaFile);
        mediaFile = new MediaFile("haha.mp4", "test video.");
        folder.add(mediaFile);
        AbstractComponent packageFile = new PackageFile("yanbo.apk", "an android application.");
        folder.add(packageFile);
        BaseFolder childDir = new BaseFolder("word", "office dir.");
        AbstractComponent officeFile = new OfficeFile("rrrr.doc", "office doc file.");
        childDir.add(officeFile);
        folder.add(childDir);
        System.out.println(folder.getString());
    }
}
           

上例通過組合模式代碼具有了良好的可擴充性,在增加新的檔案類型時,無須修改現有類庫代碼,隻需增加一個新的檔案類作為AbstractFile類的子類即可(不用在BaseFolder中再去修改添加),但是由于在AbstractFile中聲明了大量用于管理和通路成員構件的方法,例如add(),我們需要在新增的檔案類中實作這些方法,提供對應的錯誤提示和異常處理。這下你就明白了,其實如上代碼還可以繼續優化的,把每個子類實作的錯誤提示和異常處理寫入抽象的基類中作為預設處理,這樣也可以減少代碼重複。哈哈。

總結一把

當發現需求中是展現部分與整體層次結構時,以及你希望使用者可以忽略組合對象與單個對象的不同,統一地使用組合結構中的所有對象時,就應該考慮組合模式了。

組合模式優點如下:

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

組合模式缺點如下:

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

【工匠若水 http://blog.csdn.net/yanbober】 繼續閱讀《設計模式(結構型)之裝飾者模式(Decorator Pattern)》 http://blog.csdn.net/yanbober/article/details/45395747