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