前言
组合模式,类结构模式的一种。在《设计模式 - 可复用的面向对象软件》一书中将之描述为“ 将对象组合成树状结构以表示 “部分-整体” 的层次结构,使得用户对单个对象和组合对象的使用具有一致性 ”。
工作中我们经常会接触到一个对象中包含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)