c#组合模式详解
一、基础介绍:
组合模式用于表示部分-整体的层次结构。适用于希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象的情况。
顾名思义,什么叫部分-整体,比如常见的前端UI,一个DIV标签中可以存在多个A标签、P标签、DIV标签等等。
相较于DIV这个容器整体而言,其中所含的A标签、P标签甚至是DIV标签都是单个的部分。
而显示的时候却是一视同仁,不分部分还是整体。
这就是典型的组合模式。
再比如WinForms应用程序中,Label、TextBox等这样简单的控件,可以理解为节点对象,它们中无法再插入其他控件,它们就是最小的。
而比如GroupBox、DataGrid这样由多个简单控件组成的复合控件或者容器,就可以理解为容器对象,它们中可以再插入其他的节点对象,甚至是再插入其他容器对象。
但不管是Label这种节点对象还是DataGrid这种容器对象,想要显示的话都需要执行OnPaint方法。
为了表示这种对象之间整体与部分的层次结构,System.Windows.Forms.Control类就是应用了这种组合模式。
这样就可以简单的把组合模式分为三个部分:
- 抽象组件类(Component):它可以是接口或抽象类,为节点组件和容器组件对象声明接口,在该类中包含共有行为的声明。在抽象组件类中,定义了访问及管理它的子组件的方法。
- 节点组件类(Leaf):节点对象为最小组件(可以理解为树叶),并继承自抽象组件类,实现其共有声明和方法。
- 容器组件类(Composite):容器对象可以包含无数节点对象和无数容器组件(可以理解为树枝,可以有无数树叶或者分支),容器对象需要实现管理子对象的方法,如Add、Remove等。
二、应用场景:
当发现需求中是体现部分与整体层次的结构时,以及你希望用户可以忽略组合对象与单个对象的不同,统一地使用组合结构中的所有对象时,就应该考虑使用组合模式了。
UI的一系列控件就是使用了组合模式,整体和部分可以被一致对待。
组合模式有时候又叫做部分-整体模式,它使我们树型结构的问题中,模糊了简单元素和复杂元素的概念,客户程序可以向处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素的内部结构解耦。
以下情况下适用Composite模式:
1.对象的部分-整体层次结构。
2.忽略组合对象与单个对象的不同,统一地使用组合结构中的所有对象。
三、创建方式:
组合模式实现的最关键的地方是——简单对象和复合对象必须实现相同的接口。这就是组合模式能够将组合对象和简单对象进行一致处理的原因。
组合模式有两种实现方式,一种是:透明式的组合模式,另外一种是:安全式的组合模式。
透明方式————————————————
Leaf叶类中也有Add 与 Remove方法,这种方式叫透明方式。
也就是说在Component中声明所有用来管理子对象的方法,其中包括Add、Remove等。
这样实现Component接口的所有子类都具备了Add与Remove。
这样做的好处是叶节点和枝节点对于外界没有区别,它们具有一致的行为接口。
但问题也很明显,因为Leaf类本身不具备Add、Remove方法的功能,其实现是没有意义的。
安全方式————————————————
在Component接口中不去声明Add与Remove方法,那么子类Leaf也就不用必须实现它们,而在Composite类中声明所有用来管理子类对象的方法。
以文档管理器为例,文件夹为Composite,各类文档为Leaf。
- 透明方式
1).抽象类
1 /// <summary> 2 /// 抽象组件类(Component) 3 /// </summary> 4 public abstract class DocumentComponent 5 { 6 public string Name { get; set; } 7 protected List<DocumentComponent> mChildren; 8 public List<DocumentComponent> Children 9 { 10 get { return mChildren; } 11 } 12 public DocumentComponent(string name) 13 { 14 this.Name = name; 15 mChildren = new List<DocumentComponent>(); 16 } 17 18 19 public abstract void AddChild(DocumentComponent document); 20 21 public abstract void RemoveChild(DocumentComponent document); 22 23 public abstract void Show(); 24 }
接口或抽象类,为节点组件和容器组件对象声明接口,在该类中包含共有行为的声明。
在抽象组件类中,定义了访问及管理它的子组件的方法。
本实例中Show为节点和容器组件共有方法,AddChild和RemoveChild为容器组件方法。
本类主要是为了让节点类和容器类进行继承方便统一管理。
2).节点组件类
1 /// <summary> 2 /// 节点组件类(Leaf),各类文档,每类型可以添加一个对应类。 3 /// </summary> 4 public sealed class Word : DocumentComponent 5 { 6 public Word(string name) 7 : base(name) 8 { } 9 public override void AddChild(DocumentComponent document) 10 { 11 throw new Exception("节点类不支持"); 12 } 13 14 public override void RemoveChild(DocumentComponent document) 15 { 16 throw new Exception("节点类不支持"); 17 } 18 19 public override void Show() 20 { 21 Console.WriteLine("这是一篇word文档:" + Name); 22 } 23 }
节点对象为最小组件(可以理解为树叶),并继承自抽象组件类,实现show方法。
AddChild和RemoveChild为容器组件方法,在节点类中抛出异常即可。
该类是最小单位,没有子节点。
本类一个word文档对象,如果有多个类型的文档,可以声明多个类。
3).容器组件类
1 /// <summary> 2 /// 容器组件类(Composite),文件夹 3 /// </summary> 4 public class Folder : DocumentComponent 5 { 6 public Folder(string name) 7 : base(name) 8 { } 9 public override void AddChild(DocumentComponent document) 10 { 11 mChildren.Add(document); 12 Console.WriteLine("文档或文件夹增加成功"); 13 } 14 public override void RemoveChild(DocumentComponent document) 15 { 16 mChildren.Remove(document); 17 Console.WriteLine("文档或文件夹删除成功"); 18 } 19 public override void Show() 20 { 21 Console.WriteLine("这是一个文件夹:" + Name); 22 } 23 }
容器对象可以包含无数节点对象和无数容器组件(可以理解为树枝,可以有无数树叶或者分支),容器对象需要实现管理子对象的方法,如AddChild、RemoveChild等。
本类是一个文件夹对象。
4).客户端
1 /// <summary> 2 /// 客户端 3 /// </summary> 4 class Client 5 { 6 /// <summary> 7 /// 广度优先检索 8 /// </summary> 9 /// <param name="component"></param> 10 private static void BreadthFirstSearch(DocumentComponent component) 11 { 12 Queue<DocumentComponent> q = new Queue<DocumentComponent>(); 13 q.Enqueue(component); 14 Console.WriteLine(component.Name); 15 while (q.Count > 0) 16 { 17 DocumentComponent temp = q.Dequeue(); 18 List<DocumentComponent> children = temp.Children; 19 foreach (DocumentComponent child in children) 20 { 21 Console.WriteLine(child.Name); 22 q.Enqueue(child); 23 } 24 } 25 } 26 27 /// <summary> 28 /// 深度优先检索 29 /// </summary> 30 /// <param name="component"></param> 31 private static void DepthFirstSearch(DocumentComponent component) 32 { 33 Console.WriteLine(component.Name); 34 List<DocumentComponent> children = component.Children; 35 if (children == null || children.Count == 0) return; 36 foreach (DocumentComponent child in children) 37 { 38 DepthFirstSearch(child); 39 } 40 } 41 42 static void Main(string[] args) 43 { 44 Console.WriteLine("创建三个目录:"); 45 Folder folder = new Folder("根目录"); 46 Folder folder1 = new Folder("子目录1"); 47 Folder folder2 = new Folder("子目录2"); 48 49 Console.WriteLine("\r\n创建两个文档:"); 50 Word word1 = new Word("word文档1"); 51 Word word2 = new Word("word文档2"); 52 53 Console.WriteLine("\r\n将子目录1添加到根目录下:"); 54 folder.AddChild(folder1); 55 Console.WriteLine("\r\n将子目录2添加到子目录1下:"); 56 folder1.AddChild(folder2); 57 58 Console.WriteLine("\r\n将word文档1添加到子目录2下:"); 59 folder2.AddChild(word1); 60 Console.WriteLine("\r\n将word文档2添加到根目录下:"); 61 folder.AddChild(word2); 62 63 Console.WriteLine("\r\n广度优先列表:"); 64 DepthFirstSearch(folder); 65 Console.WriteLine("\r\n深度优先列表:"); 66 BreadthFirstSearch(folder); 67 68 Console.ReadKey(); 69 } 70 71 72 }
注:BreadthFirstSearch为广度优先检索,依次列出所有元素。DepthFirstSearch为深度优先检索,列举完一个文件夹后,返回根目录继续列举其他文件夹。
通过上述实例可以看出,文件夹可以创建N个子文件夹,但文档只能放在文件夹中,无法放在另一个文档中。
- 安全方式
1 /// <summary> 2 /// 抽象组件类(Component) 3 /// </summary> 4 public abstract class DocumentComponent 5 { 6 public string Name { get; set; } 7 protected List<DocumentComponent> mChildren; 8 public List<DocumentComponent> Children 9 { 10 get { return mChildren; } 11 } 12 public DocumentComponent(string name) 13 { 14 this.Name = name; 15 mChildren = new List<DocumentComponent>(); 16 } 17 18 public abstract void Show(); 19 } 20 21 /// <summary> 22 /// 节点组件类(Leaf),各类文档,每类型可以添加一个对应类。 23 /// </summary> 24 public sealed class Word : DocumentComponent 25 { 26 public Word(string name) 27 : base(name) 28 { } 29 30 public override void Show() 31 { 32 Console.WriteLine("这是一篇word文档:" + Name); 33 } 34 } 35 36 /// <summary> 37 /// 容器组件类(Composite),文件夹 38 /// </summary> 39 public class Folder : DocumentComponent 40 { 41 public Folder(string name) 42 : base(name) 43 { } 44 public void AddChild(DocumentComponent document) 45 { 46 mChildren.Add(document); 47 Console.WriteLine("文档或文件夹增加成功"); 48 } 49 public void RemoveChild(DocumentComponent document) 50 { 51 mChildren.Remove(document); 52 Console.WriteLine("文档或文件夹删除成功"); 53 } 54 public override void Show() 55 { 56 Console.WriteLine("这是一个文件夹:" + Name); 57 } 58 } 59 60 61 /// <summary> 62 /// 客户端 63 /// </summary> 64 class Client 65 { 66 /// <summary> 67 /// 广度优先检索 68 /// </summary> 69 /// <param name="component"></param> 70 private static void BreadthFirstSearch(DocumentComponent component) 71 { 72 Queue<DocumentComponent> q = new Queue<DocumentComponent>(); 73 q.Enqueue(component); 74 Console.WriteLine(component.Name); 75 while (q.Count > 0) 76 { 77 DocumentComponent temp = q.Dequeue(); 78 List<DocumentComponent> children = temp.Children; 79 foreach (DocumentComponent child in children) 80 { 81 Console.WriteLine(child.Name); 82 q.Enqueue(child); 83 } 84 } 85 } 86 87 /// <summary> 88 /// 深度优先检索 89 /// </summary> 90 /// <param name="component"></param> 91 private static void DepthFirstSearch(DocumentComponent component) 92 { 93 Console.WriteLine(component.Name); 94 List<DocumentComponent> children = component.Children; 95 if (children == null || children.Count == 0) return; 96 foreach (DocumentComponent child in children) 97 { 98 DepthFirstSearch(child); 99 } 100 } 101 102 static void Main(string[] args) 103 { 104 Console.WriteLine("创建三个目录:"); 105 Folder folder = new Folder("根目录"); 106 Folder folder1 = new Folder("子目录1"); 107 Folder folder2 = new Folder("子目录2"); 108 109 Console.WriteLine("\r\n创建两个文档:"); 110 Word word1 = new Word("word文档1"); 111 Word word2 = new Word("word文档2"); 112 113 Console.WriteLine("\r\n将子目录1添加到根目录下:"); 114 folder.AddChild(folder1); 115 Console.WriteLine("\r\n将子目录2添加到子目录1下:"); 116 folder1.AddChild(folder2); 117 118 Console.WriteLine("\r\n将word文档1添加到子目录2下:"); 119 folder2.AddChild(word1); 120 Console.WriteLine("\r\n将word文档2添加到根目录下:"); 121 folder.AddChild(word2); 122 123 Console.WriteLine("\r\n广度优先列表:"); 124 DepthFirstSearch(folder); 125 Console.WriteLine("\r\n深度优先列表:"); 126 BreadthFirstSearch(folder); 127 128 Console.ReadKey(); 129 } 130 131 }
从上述实例中可以看出,安全模式其实就是把共有的方法放在抽象类的。
文件夹独有的方法放在容器类中,这样做保证了节点类就没有Add和Remove等无用方法。
四、总结:
组合模式解耦了客户程序与复杂元素内部结构,从而使客户程序可以向处理简单元素一样来处理复杂元素。