二十三種設計模式[8] - 組合模式(Composite Pattern)


前言      

組合模式,類結構模式的一種。在《設計模式 - 可復用的面向對象軟件》一書中將之描述為“ 將對象組合成樹狀結構以表示 “部分-整體” 的層次結構,使得用戶對單個對象和組合對象的使用具有一致性 ”。

       工作中我們經常會接觸到一個對象中包含0個或多個其它對象,而其它對象依然包含0個或多個其它對象,這種結構我們稱之為樹狀結構。組合模式就是通過遞歸去幫助我們去管理這類樹狀結構。

結構

Compsite_3

需要角色如下:

  • Component(所有節點的抽象):所有對象(節點)的抽象或接口,用來定義所有節點的行為;
  • Leaf(葉節點):樹狀結構中的葉節點(沒有子節點),繼承抽象並實現行為;
  • Composite(根節點):樹狀結構中的根節點和子樹的根節點,葉節點的容器,用來管理子節點;

場景

       最經典的樹狀結構莫過於操作系統中的文件目錄結構。我們都知道在一個文件夾中會包含0個或多個文件,而這些文件中又會包含0個或多個文件。如下。

Compsite_

       在設計它的結構時往往會增加一個文件夾類,並根據文件夾內的文件類型維護相應的List去存儲,以此類推。也就是說在文件夾類中,我們會根據不同的類型去創建不同的List,每當文件夾支持新的類型時我們都要去修改這個文件夾類,並不符合開閉原則。而且隨着文件夾類支持的類型越多,這個類也將變得越來越復雜。

       使用組合模式使得我們在編碼過程中不必過分關注各個文件的類型(只要是一個文件),並通過遞歸來簡化文件夾類的設計。如下。

Compsite_2

示例

Compsite_4

public interface IFile
{
    IFile Father { set; get; }
    bool IsFolder { get; }
    string ShowMyself();
    IFile GetChild(int index);
    void Add(IFile obj);
    void Remove(IFile obj);
}

public class Txt : IFile
{
    public bool IsFolder => false;
    public string Name { set; get; } = string.Empty;
    public IFile Father { set; get; }

    public Txt(string name)
    {
        this.Name = name;
    }

    public string ShowMyself()
    {
        string spec = string.Empty;
        IFile father = this.Father;
        while (father != null)
        {
            spec += "  ";
            father = father.Father;
        }
        return $"{spec + this.Name}.txt";
    }

    public IFile GetChild(int index)
    {
        throw new NotImplementedException("Sorry,I have not Child");
    }

    public void Add(IFile obj)
    {
        throw new NotImplementedException("Sorry,I have not Child");
    }

    public void Remove(IFile obj)
    {
        throw new NotImplementedException("Sorry,I have not Child");
    }
}

public class Png : IFile
{
    public bool IsFolder => false;
    public string Name { set; get; } = string.Empty;
    public IFile Father { set; get; }

    public Png(string name)
    {
        this.Name = name;
    }

    public string ShowMyself()
    {
        string spec = string.Empty;
        IFile father = this.Father;
        while (father != null)
        {
            spec += "  ";
            father = father.Father;
        }
        return $"{spec + this.Name}.png";
    }

    public IFile GetChild(int index)
    {
        throw new NotImplementedException("Sorry,I have not child");
    }

    public void Add(IFile obj)
    {
        throw new NotImplementedException("Sorry,I have not child");
    }

    public void Remove(IFile obj)
    {
        throw new NotImplementedException("Sorry,I have not child");
    }
}

public class Folder : IFile
{
    public bool IsFolder => true;
    public string Name { set; get; } = string.Empty;
    public IFile Father { set; get; }
    private List<IFile> _childList = new List<IFile>();

    public Folder(string name)
    {
        this.Name = name;
    }

    public string ShowMyself()
    {
        string spec = string.Empty;
        IFile father = this.Father;
        while (father != null)
        {
            spec += "  ";
            father = father.Father;
        }

        string result = spec + this.Name;
        foreach (IFile child in _childList)
        {
            result += Environment.NewLine + child.ShowMyself();
        }

        return result;
    }

    public IFile GetChild(int index)
    {
        if(index >= this._childList.Count)
        {
            throw new Exception("越界");
        }

        return this._childList[index];
    }

    public void Add(IFile obj)
    {
        IFile father = this;
        while(father != null)
        {
            if(object.ReferenceEquals(obj, father))
            {
                throw new Exception("循環引用");
            }

            father = father.Father;
        }

        if(this._childList.Exists(t=> object.ReferenceEquals(t, obj)))
        {
            throw new Exception("子節點已存在");
        }

        obj.Father = this;
        this._childList.Add(obj);
    }

    public void Remove(IFile obj)
    {
        if(obj.Father == null
            || !this._childList.Exists(t=> object.ReferenceEquals(t, obj)))
        {
            throw new Exception("未找到子節點");
        }

        obj.Father = null;
        this._childList.Remove(obj);
    }
}

static void Main(string[] args)
{
    IFile folder = new Folder("我的文檔");
    IFile txtFileA = new Txt("新建文本文檔A");
    IFile pngFileA = new Png("QQ截圖A");
    IFile folderA = new Folder("新建文件夾A");

    if (folder.IsFolder)
    {
        folder.Add(txtFileA);
        folder.Add(pngFileA);
        folder.Add(folderA);
    }

    IFile txtFileB = new Txt("新建文本文檔B");
    IFile pngFileB = new Png("QQ截圖B");

    if (folderA.IsFolder)
    {
        folderA.Add(txtFileB);
        folderA.Add(pngFileB);
    }
    
    Console.WriteLine(folder.ShowMyself());
    Console.ReadKey();
}

image

       在示例中,IFile接口定義了IFile類型的屬性(在C#里,接口中可以定義屬性)用來存儲父節點,方便結構的向上操作。函數IsFolder用來標識當前對象是否是一個Composite。ShowMyself函數表示各個節點的基本操作,在Composite角色(Folder類)中一般遞歸調用子節點的ShowMyself函數。Add、Remove以及GetChild函數用來管理子節點。

       注意:管理子節點的操作函數是在組合模式中比較有爭議的一個點。我們不難看出,對於葉節點(類Txt、Png)來說管理子節點的操作是沒有意義的(因為它們沒有子節點)。在IFile接口中聲明這些操作能夠保證節點的一致性以及結構的透明性,但會使調用者做一些無意義的操作(比如調用Txt類的Add函數)。而在Composite角色中定義這些操作雖然能夠避免調用者的無意義操作,但會使節點的透明性和一致性降低。

       由於組合模式更加強調各個節點的一致性以及通明性,這里更加推薦在接口中定義那些管理子節點的函數。

       為了減少葉節點重復的實現這些對它無意義的子節點管理函數,可以使用適配器模式 (Adapter)對IFile接口做一個適配,為函數提供一個缺省的實現並使所有葉節點繼承這個適配器。或者將IFile聲明為一個抽象類並為函數提供缺省的實現。

總結

       在組合模式中,通過定義節點的公共接口提高結構的一致性以及透明性,並通過遞歸來簡化類的設計。在我們對結構進行擴展時,只需要增加接口的實現類而無需對現有代碼進行改動,符合開閉原則。但在使用的過程中,我們很難實現對各個節點的約束,並且遞歸的使用使得我們需要花費更多的時間去理解它的層次關系。遞歸的使用也使得我們需要更加謹慎的處理結構的深度,以免造成內存溢出。

 

       以上,就是我對組合模式的理解,希望對你有所幫助。

       示例源碼:https://gitee.com/wxingChen/DesignPatternsPractice

       系列匯總:https://www.cnblogs.com/wxingchen/p/10031592.html

       本文著作權歸本人所有,如需轉載請標明本文鏈接(https://www.cnblogs.com/wxingchen/p/10078594.html)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM