一、引出模式
在软件开发中,我们经常会遇到树型目录的功能,比如:管理商品的目录
如果让你来实现这个功能,你会怎么做呢?
我们先来分析分析:商品类别树上的节点有三类,根节点、树枝节点和叶子节点,在进一步根节点和树枝节点都是可以包含其他节点的,我们就叫它容器节点。这样,商品类别树就分为了容器节点和叶子节点,我们将它们分别实现成为对象。
代码示例:
class Program { static void Main(string[] args) { //定义所有的组合对象 Composite root = new Composite("服装"); Composite c1 = new Composite("男装"); Composite c2 = new Composite("女装"); //定义所有的叶子对象 Leaf leaf1 = new Leaf("衬衣"); Leaf leaf2 = new Leaf("夹克"); Leaf leaf3 = new Leaf("裙子"); Leaf leaf4 = new Leaf("套装"); //按照树的结构来组合组合对象和叶子对象 root.AddComposite(c1); root.AddComposite(c2); c1.AddLeaf(leaf1); c1.AddLeaf(leaf2); c2.AddLeaf(leaf3); c2.AddLeaf(leaf4); //调用根对象的输出功能来输出整棵树 root.PrintStruct(""); Console.ReadKey(); } } /// <summary> /// 叶子对象 /// </summary> public class Leaf { /// <summary> /// 叶子对象的名字 /// </summary> private string name = null; /// <summary> /// 构造方法,传入叶子对象的名字 /// </summary> /// <param name="name">叶子对象的名字</param> public Leaf(string name) { this.name = name; } /// <summary> /// 输出叶子对象的结构,叶子对象没有子对象,也就是输出叶子对象的名字 /// </summary> /// <param name="preStr">前缀,主要是按照层级拼接的空格,实现向后缩进</param> public void PrintStruct(string preStr) { Console.WriteLine(preStr + "-" + name); } } /// <summary> /// 组合对象,可以包含其它组合对象或者叶子对象 /// </summary> public class Composite { /// <summary> /// 用来记录包含的其它组合对象 /// </summary> private List<Composite> childComposite = new List<Composite>(); /// <summary> /// 用来记录包含的其它叶子对象 /// </summary> private List<Leaf> childLeaf = new List<Leaf>(); /// <summary> /// 组合对象的名字 /// </summary> private string name = null; /// <summary> /// 构造方法,传入组合对象的名字 /// </summary> /// <param name="name"></param> public Composite(string name) { this.name = name; } /// <summary> /// 向组合对象加入被它包含的其它组合对象 /// </summary> /// <param name="c"></param> public void AddComposite(Composite c) { this.childComposite.Add(c); } /// <summary> /// 向组合对象加入被它包含的叶子对象 /// </summary> /// <param name="leaf"></param> public void AddLeaf(Leaf leaf) { this.childLeaf.Add(leaf); } /// <summary> /// 输出组合对象自身的结构 /// </summary> /// <param name="preStr"></param> public void PrintStruct(String preStr) { //先把自己输出去 Console.WriteLine(preStr + "+" + this.name); //然后添加一个空格,表示向后缩进一个空格,输出自己包含的叶子对象 preStr += " "; foreach (Leaf leaf in childLeaf) { leaf.PrintStruct(preStr); } //输出当前对象的子对象了 foreach (Composite c in childComposite) { ////递归输出每个子对象 c.PrintStruct(preStr); } } }
功能上已经实现好了,但有何问题呢?
区分了组合对象和叶子对象,并进行有区别的对待,比如在Composite和Client里面,都需要区别对待这两种对象,这就是个问题。
对于这种具有整体与部分关系,并能组合成树型结构的对象结构,如何才能够以一个统一的方式来进行操作呢?
二、认识模式
1.模式定义
将对象组合成为属性结构以表示“整体-部分”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
2.解决思路
上述例子中,要区分组合对象和叶子对象,就是因为没有把组合对象和叶子对象统一起来。
组合模式通过引入一个抽象的组件对象,作为组合对象和叶子对象的父对象,这样就把组合对象和叶子对象统一起来,用户使用时,始终是在操作组件对象,而不用再区分是在操作组合对象还是叶子对象。
3.模式图示
Component:抽象的组件对象,为组合中的对象声明接口,让客户端可以通过这个接口来访问和管理整个对象结构,可以在里面为定义的功能提供缺省的实现。
Leaf:叶子节点对象,定义和实现叶子对象的行为,不再包含其它的子节点对象。
Composite:组合对象,通常会存储子组件,定义包含子组件的那些组件的行为,并实现在组件接口中定义的与子组件有关的操作。
Client:客户端,通过组件接口来操作组合结构里面的组件对象。
4.模式原型示例代码
class Program { static void Main(string[] args) { //定义多个Composite对象 Component root = new Composite(); Component c1 = new Composite(); Component c2 = new Composite(); //定义多个叶子对象 Component leaf1 = new Leaf(); Component leaf2 = new Leaf(); Component leaf3 = new Leaf(); //组和成为树形的对象结构 root.AddChild(c1); root.AddChild(c2); root.AddChild(leaf1); c1.AddChild(leaf2); c2.AddChild(leaf3); //操作Component对象 Component o = root.GetChildren(1); Console.WriteLine(o); } /// <summary> /// 抽象的组件对象,为组合中的对象声明接口,实现接口的缺省行为 /// </summary> public abstract class Component { /// <summary> /// 示意方法,子组件对象可能有的功能方法 /// </summary> public abstract void SomeOperation(); /// <summary> /// 向组合对象中加入组件对象 /// </summary> /// <param name="component"></param> public virtual void AddChild(Component component) { } /// <summary> /// 从组合对象中移出某个组件对象 /// </summary> /// <param name="component"></param> public virtual void RemoveChild(Component component) { } /// <summary> /// 返回某个索引对应的组件对象 /// </summary> /// <param name="index"></param> /// <returns></returns> public virtual Component GetChildren(int index) { } } /// <summary> /// 叶子对象,叶子对象不再包含其它子对象 /// </summary> public class Leaf : Component { /// <summary> /// 示意方法,叶子对象可能有自己的功能方法 /// </summary> public override void SomeOperation() { // do something } } /// <summary> /// 组合对象,通常需要存储子对象,定义有子部件的部件行为, /// 并实现在Component里面定义的与子部件有关的操作 /// </summary> public class Composite : Component { /// <summary> /// 用来存储组合对象中包含的子组件对象 /// </summary> private List<Component> childComponents = null; /// <summary> /// 示意方法,通常在里面需要实现递归的调用 /// </summary> public override void SomeOperation() { if (childComponents != null) { foreach (Component c in childComponents) { //递归的进行子组件相应方法的调用 c.SomeOperation(); } } } public override void AddChild(Component component) { //延迟初始化 if (childComponents == null) { childComponents = new List<Component>(); } childComponents.Add(component); } public override void RemoveChild(Component component) { if (childComponents != null) { childComponents.Remove(component); } } public override Component GetChildren(int index) { if (childComponents != null) { if (index >= 0 && index < childComponents.Count) { return childComponents[index]; } } return null; } } }
5.商品分类目录实例代码
class Program { static void Main(string[] args) { //定义所有的组合对象 Component root = new Composite("服装"); Component c1 = new Composite("男装"); Component c2 = new Composite("女装"); //定义所有的叶子对象 Component leaf1 = new Leaf("衬衣"); Component leaf2 = new Leaf("夹克"); Component leaf3 = new Leaf("裙子"); Component leaf4 = new Leaf("套装"); //按照树的结构来组合组合对象和叶子对象 root.AddChild(c1); root.AddChild(c2); c1.AddChild(leaf1); c1.AddChild(leaf2); c2.AddChild(leaf3); c2.AddChild(leaf4); //调用根对象的输出功能来输出整棵树 root.PrintStruct(""); Console.ReadKey(); } public abstract class Component { public abstract void PrintStruct(string preStr); public virtual void AddChild(Component component) { } public virtual void RemoveChild(Component component) { } public virtual Component GetChildren(int index) { return null; } } public class Leaf : Component { private string name = null; public Leaf(string name) { this.name = name; } public override void PrintStruct(string preStr) { Console.WriteLine(preStr + "-" + name); } } public class Composite : Component { private string name = null; private List<Component> childComponents = null; public Composite(string name) { this.name = name; } public override void PrintStruct(string preStr) { Console.WriteLine(preStr + "+" + name); if (childComponents != null) { preStr += " "; foreach (var c in childComponents) { c.PrintStruct(preStr); } } } public override void AddChild(Component component) { if (childComponents == null) { childComponents = new List<Component>(); } childComponents.Add(component); } public override void RemoveChild(Component component) { if (childComponents == null) { childComponents = new List<Component>(); } childComponents.Remove(component); } public override Component GetChildren(int index) { if (childComponents != null) { if (index > 0 && index < childComponents.Count) { return childComponents[index]; } } return null; } } }
三、理解模式
1.组合模式的目的
组合模式的目的是:让客户端不再区分操作的是组合对象还是叶子对象,而是以一种统一的方式来操作
实现这个目标的关键之处,是设计一个抽象的组件类,让它可以代表组合对象和叶子对象。
2.对象树
组合模式会组合出树型结构,组成这个树型结构所使用的多个组件对象,就自然形成的对象树。
所有可以使用对象树来描述或操作的功能,都可以考虑组合模式。比如读取XML,或对语句进行语法解析等。
3.组合模式中的递归
组合模式中的递归,是对象本身的递归,是对象的组合方式,从设计上来讲是递归关联,是对象关联关系的一种。
4.安全性和透明性
在组合模式中,把组件对象分为两种:一种是可以包含子组件Composite对象;另一种不能包含其他组件对象的叶子对象。
Composite对象就像是一个容器,可以包含其他的Composite对或叶子对象。有了容器,就要对容器进行维护和管理。
这就产生了这样一个问题:在组合模式的类层次结构中,到底哪一些类里面定义这些管理子组件的操作,是应该在Component中声明这些操作呢,还是在Composite中声明这些操作?
这就需要仔细思考,在不同的实现中,进行安全性和透明性的权衡选择。
这里所说的安全性是指:从客户使用组合模式上看是否更安全。如果是安全的,那么不会有发生误操作的可能,能访问的方法都是被支持的功能。
这里所说的透明性是指:从客户使用组合模式上,是否需要区分到底是组合对象还是叶子对象。如果是透明的,那就是不再区分,对于客户而言,都是组件对象,具体的类型对于客户而言是透明的,是客户无需要关心的。
透明性的实现
如果把管理子组件的操作定义在Component中,那么客户端只需要面对Component,而无需关心具体的组件类型,这种实现方式就是透明性的实现。事实上,前面示例的实现方式都是这种实现方式。
但是透明性的实现是以安全性为代价的,因为在Component中定义的一些方法,对于叶子对象来说是没有意义的,比如:增加、删除子组件对象。而客户不知道这些区别,对客户是透明的,因此客户可能会对叶子对象调用这种增加或删除子组件的方法,这样的操作是不安全的。
组合模式的透明性实现,通常的方式是:在Component中声明管理子组件的操作,并在Component中为这些方法提供缺省的实现,如果是有子对象不支持的功能,缺省的实现可以是抛出一个例外,来表示不支持这个功能。
安全性的实现
如果把管理子组件的操作定义在Composite中,那么客户在使用叶子对象的时候,就不会发生使用添加子组件或是删除子组件的操作了,因为压根就没有这样的功能,这种实现方式是安全的。
但是这样一来,客户端在使用的时候,就必须区分到底使用的是Composite对象,还是叶子对象,不同对象的功能是不一样的。也就是说,这种实现方式,对客户而言就不是透明的了。
5.组合模式的优缺点
定义了包含基本对象和组合对象的类层次结构
在组合模式中,基本对象可以被组合成更复杂的组合对象,而组合对象又可以组合成更复杂的组合对象,可以不断地递归组合下去,从而构成一个统一的组合对象的类层次结构
统一了组合对象和叶子对象
在组合模式中,可以把叶子对象当作特殊的组合对象看待,为它们定义统一的父类,从而把组合对象和叶子对象的行为统一起来
简化了客户端调用
组合模式通过统一组合对象和叶子对象,使得客户端在使用它们的时候,就不需要再去区分它们,客户不关心使用的到底是什么类型的对象,这就大大简化了客户端的使用
更容易扩展
由于客户端是统一的面对Component来操作,因此,新定义的Composite或Leaf子类能够很容易的与已有的结构一起工作,而客户端不需要为增添了新的组件类而改变
很难限制组合中的组件类型
容易增加新的组件也会带来一些问题,比如很难限制组合中的组件类型。这在需要检测组件类型的时候,使得我们不能依靠编译期的类型约束来完成,必须在运行期间动态检测。
6.何时选用组合模式
建议在如下情况中,选用组合模式:
如果你想表示对象的部分-整体层次结构,可以选用组合模式,把整体和部分的操作统一起来,使得层次结构实现更简单,从外部来使用这个层次结构也简单
如果你希望统一的使用组合结构中的所有对象,可以选用组合模式,这正是组合模式提供的主要功能
7.组合模式的本质
组合模式的本质:统一叶子对象和组合对象。
组合模式通过把叶子对象当成特殊的组合对象看待,从而对叶子对象和组合对象一视同仁,统统当成了Component对象,有机的统一了叶子对象和组合对象。
正是因为统一了叶子对象和组合对象,在将对象构建成树形结构的时候,才不需要做区分,反正是组件对象里面包含其它的组件对象,如此递归下去;也才使得对于树形结构的操作变得简单,不管对象类型,统一操作。