前言
組合模式,類結構模式的一種。在《設計模式 - 可復用的面向對象軟件》一書中將之描述為“ 將對象組合成樹狀結構以表示 “部分-整體” 的層次結構,使得用戶對單個對象和組合對象的使用具有一致性 ”。
工作中我們經常會接觸到一個對象中包含0個或多個其它對象,而其它對象依然包含0個或多個其它對象,這種結構我們稱之為樹狀結構。組合模式就是通過遞歸去幫助我們去管理這類樹狀結構。
結構
需要角色如下:
- Component(所有節點的抽象):所有對象(節點)的抽象或接口,用來定義所有節點的行為;
- Leaf(葉節點):樹狀結構中的葉節點(沒有子節點),繼承抽象並實現行為;
- Composite(根節點):樹狀結構中的根節點和子樹的根節點,葉節點的容器,用來管理子節點;
場景
最經典的樹狀結構莫過於操作系統中的文件目錄結構。我們都知道在一個文件夾中會包含0個或多個文件,而這些文件中又會包含0個或多個文件。如下。
在設計它的結構時往往會增加一個文件夾類,並根據文件夾內的文件類型維護相應的List去存儲,以此類推。也就是說在文件夾類中,我們會根據不同的類型去創建不同的List,每當文件夾支持新的類型時我們都要去修改這個文件夾類,並不符合開閉原則。而且隨着文件夾類支持的類型越多,這個類也將變得越來越復雜。
使用組合模式使得我們在編碼過程中不必過分關注各個文件的類型(只要是一個文件),並通過遞歸來簡化文件夾類的設計。如下。
示例
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(); }
在示例中,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)