前言
組合模式,類結構模式的一種。在《設計模式 - 可復用的面向對象軟件》一書中將之描述為“ 將對象組合成樹狀結構以表示 “部分-整體” 的層次結構,使得用戶對單個對象和組合對象的使用具有一致性 ”。
工作中我們經常會接觸到一個對象中包含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)





