学习设计模式《十四》——组合模式
一、基础概念
组合模式的本质是【统一叶子对象和组合对象】;
组合模式的定义:将对象组合成树型结构以表示“部分-整体”的层次结构;组合模式使得用户对单个对象和组合对象的使用具有一致性。
序号 | 认识组合模式 | 说明 |
1 | 组合模式的目的 | 让客户端不再区分操作的是组合对象还是叶子对象,而是以一个统一的方式来操作;实现这个目标的关键之处就是:设计一个抽象的组件类,让它可以代表组合对象和叶子对象,这样一来,客户端就不用区分到底是 组合对象还是叶子对象了,只需要把它们全部当作组件对象进行统一的操作就可以了 |
2 | 对象树 | 通常,组合模式会组合出树型结构来,组成这个树型结构所使用的多个组件对象就自然的形成了对象树。这也意味着,所有可以使用对象树来描述或操作的功能,都可以考虑使用组合模式【如:读取XML文件,或是对语句进行语法解析】 |
3 | 组合模式中的递归 | 指的是对象递归组合【是对象本身的递归,是对象组合的方式,从设计上来说是递归关联,是对象关系的一种;且理论上没有层次限制】,不是常说的递归算法。通常我们谈论的递归算法是指“一个方法会调用方法自己”这样的算法(如:经典的求阶乘)在 |
4 | Component中是否应该实现一个Component列表 | 大多数情况下,一个Composite对象会持有子节点的集合【那么能不能把这个子节点集合定义到Component中去呢?这样一来,大部分的工作就可以在Component中完成了】(事实上,这种方法不太好,因为在父类中存放子类的实例对象 ,对于Composite节点是没有影响,因为它本来就需要存放子节点;但是对于叶子节点来说,就会导致空间的浪费,因为叶子节点本身不需要子节点;只有当组合结构中叶子对象数目较少的时候,才会使用这种方法) |
5 | 最大化Component定义 | 由于组合模式的目的是让客户端不再区分操作的是组合对象还是叶子对象,而是以一种统一的方式来操作;所以Component中的方法也主要是两种对象对外方法的和【即组件里面既有叶子对象需要的方法,也有组合对象需要的方法】(但是这种实现是与类的设计原则冲突的,类的设计原则是【一个父类应该只定义那些对它的子类有意义的操作】,那怎么解决这个冲突呢?) 《1》常见的做法是:在Component中为对某些子对象没有意义的方法提供默认的实现,或是默认抛出不支持该功能的例外(这样的话,如果子对象需要这个功能,那就覆盖实现它,如果不需要,那就不用管,使用父类的默认实现就可以了) 《2》从一个层面来说,如果把叶子对象看成是一个特殊的Composite对象,这样对于Component而言,子对象就全部看做是组合对象,因此定义的所有方法都是有意义的 |
6 | 子部件排序 | 在某些应用中,使用组合模式的时候,需要按照一定的顺序来使用子组件对象(如:进行语法分析的时候,使用组合模式构建的抽象语法树,在解析执行的时候,是需要按照顺序来执行的)对于这样的功能,在设计的时候,需要把组件对象的索引考虑进去,并仔细地设计对子节点的访问和管理接口。通常的方式是需要按照顺序来存储,这样在获取的时候就可以按照顺序得到了,可以考虑结合迭代器模式来实现按照顺序访问组件对象 |
序号 | 安全性和透明性 | 说明 |
1 | 安全性和透明性性 | 在组合模式的层次结构中,到底在哪一些类里面定义这些管理子组件的操作?是应该在Component中声明这些操作呢?还是Composite中声明这些操作呢?【在不同的实现中,进行安全性和透明性的权衡选择】 《1》安全性是指:从客户使用组合模式上看是否更安全,如果安全,那么就不会有误操作的可能,能访问的方法都是被支持的功能; 《2》透明性是指:从客户使用组合模式上,是否需要区分到底是组合对象还是叶子对象,如果透明,那就不用再区分,对于客户而言都是组件对象具体的类型对于客户而言是透明的,客户无须关心。 |
2 | 安全性的实现 | 如果把管理子组件的操作定义在Composite中,那么客户在使用叶子对象的时候,就不会发生使用添加子组件或是删除子组件的操作了, 因为压根就没有这样的功能,这样的实现是安全的。但是这样一来,客户端在使用的时候,就必须区分到底是Composite对象,还是叶子对象,不同对象的功能是不一样的( 这种实现方式,对客户而言就不是透明的了) |
3 | 透明性的实现 | 如果把管理子组件的操作定义在Component中,那么客户端只需要面对Component,而无须关系具体的组件类型,这就是透明的实现。 但是透明是的实现是以安全性为代价的,因为在Component中定义的一些方法,对于叶子对象来说是没有意义的(如增加、删除子组件对象)而客户不知道这些区别,对客户是透明的,因此客户可能会对叶子对象调用这种增加或删除子组件的方法,这样的操作是不安全的 |
4 | 两种实现方式的选择 | 对于组合模式而言,在安全性和透明性上,会更看重透明性,毕竟组合模式的功能就是要让用户对叶子对象和组合对象的使用具有一致性。 而且对于安全性的实现,需要区分是组合对象还是叶子对象。有时候,需要将对象进行类型转换,却发现类型信息丢失了,只好强行转换, 这种类型转换必然是不够安全的【对于这种情况的处理方法是:在Component中定义一个GetComposite方法,用来判断是组合对象还是叶子对象,如果是组合对象,就返回组合对象,如果是叶子对象,就返回null,这样就实现了先判断,再强制转换】。 因此在使用组合模式的时候,建议多采用透明性的实现方式,少用安全性模式。 |
序号 | 组合模式的优点 | 组合模式的缺点 |
1 | 定义了包含基本对象和组合对象的类层次结构 (在组合模式中,基本对象可以被组合成为复杂的组合对象,而组合对象又可以组合成为更复杂的组合对象,可以不断递归下去,从而构成一个统一的组合对象类层次结构) | 很难限制组合中的组件类型 |
3 | 统一了组合对象和叶子对象 (在组合模式中,可以把叶子对象当作特殊的组合对象看待,为它们定义统一的父类,从而把组合对象和叶子对象的行为统一起来) | |
3 | 简化了客户端的调用 (组合模式通过统一组合对象和叶子对象,使得客户端在使用它们的时候,不需要再去区分它们,客户不关心使用的到底是什么类型的对象,这就大大简化了客户端的使用) | |
4 | 更容易扩展 (由于客户端是统一的面对Component来操作,因此,新定义的Composite或Leaf子类就能够很容易地与已有的结构一起工作,而客户端不需要为增添了新的组件类而改变) |
何时选用组合模式?
1、如果你想表示对象的部分-整体层次结构,可以选用组合模式,把整体和部分的操作统一起来,使得层次结构实现更加简单,才能够外部来使用这个层次结构也容易;
2、如果你希望统一地使用组合结构中的所有对象,可以选用组合模式,这正是组合模式提供的主要功能。
二、组合模式示例
业务需求:现在需要实现一个管理商品类别的树,类似下图这样的效果:
该商品树具备如下特点:
《1》有一个根节点(如:服装,可以包含多个子节点);
《2》树支节点(如:男装、女装,还可以包含其他子节点);
《3》叶子节点(如:衬衣、夹克、裙子、卫衣、牛仔裤,没有任何子节点)
2.1、不使用模式的示例
分析:要管理商品类别树,就是管理各个节点(如:根节点、树节点和叶子节点)其实根节点和树节点是类似的,都可以包含其他子节点,可以把它们称为容器节点;这样一来其实商品类别树就分为两种【容器节点】(可以包含其他容器或叶子节点)【叶子节点】(只有自己没有其他节点)。
2.1.1、实现叶子对象
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace CompositePattern
{/// <summary>/// 叶子对象/// </summary>internal class Leaf{//叶子对象的名字private string name;/// <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}");}}//Class_end
}
2.1.2、组合对象的实现(包含其他组合对象和叶子节点)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace CompositePattern
{/// <summary>/// 组合对象,可以包含其他组合对象或叶子对象/// </summary>internal class Composite{//用来记录包含的其他组合对象private List<Composite> compositeList = new List<Composite>();//用来记录包含的其他叶子对象private List<Leaf> leafList = new List<Leaf>();//组合对象的名称private string name = string.Empty;/// <summary>/// 构造方法/// </summary>/// <param name="name">组合对象的名称</param>public Composite(string name){this.name = name; }/// <summary>/// 向组合对象加入被它包含的其他组合对象/// </summary>/// <param name="composite">被它包含的其他组合对象</param>public void AddComposite(Composite composite){compositeList.Add(composite);}/// <summary>/// 向组合对象加入被它包含的叶子对象/// </summary>/// <param name="leaf">被它包含的叶子对象</param>public void AddLeaf(Leaf leaf){leafList.Add(leaf);}/// <summary>/// 输出组合对象自身的结构/// </summary>/// <param name="preStr">前缀【主要是按照层级拼接的空格,实现向后缩进】</param>public void PrintStruct(string preStr){Console.WriteLine($"{preStr}+{this.name}");preStr += " ";foreach (Leaf leaf in leafList){leaf.PrintStruct(preStr);}//输出当前对象的子对象foreach (var item in compositeList){item.PrintStruct(preStr);}}}//Class_end
}
2.1.3、编写客户端测试
using System.Numerics;namespace CompositePattern
{internal class Program{static void Main(string[] args){CompositeTest();Console.ReadLine();}/// <summary>/// 测试组合/// </summary>private static void CompositeTest(){Console.WriteLine("---测试组合---");//定义所有的组合对象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("卫衣");Leaf leaf5 = new Leaf("牛仔裤");//按照树结构来组合对象和叶子对象root.AddComposite(c1);root.AddComposite(c2);c1.AddLeaf(leaf1);c1.AddLeaf(leaf2);c1.AddLeaf(leaf3);c2.AddLeaf(leaf4);c2.AddLeaf(leaf5);//调用根对象输出结构root.PrintStruct("");}}//Class_end
}
2.1.4、运行结果
在这里我们虽然实现了要求的功能,但是有一个很明显的问题【必须区分组合对象和叶子对象,并进行有区别的对待】(如:在Composite和Client里面,都需要区别对待这两种对象;这种区别对待组合对象和叶子对象不仅会让程序变得复杂,还对功能的扩展也带来不便)【实际上用户并不想区分它们,而是认为它们是一样的,这样操作起来最简单】。
2.2、使用组合模式的示例
组合模式通过引入一个抽象的组件对象,作为组合对象和叶子对象的父对象,这样就把组合对象和叶子对象统一起来了,用户使用的时候,始终在操作组件对象,而不用再区分是在操作组合对象还是叶子对象。
2.2.1、创建一个抽象的组件对象
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace CompositePattern.CompositeDemo
{/// <summary>/// 抽象的组件对象/// </summary>internal abstract class Component{//输出组件自身的名称public abstract void PrintStruct(string preStr);//向组合对象中加入组件对象public abstract void Add(Component child);//从组合对象中移除某个组件对象public abstract void Remove(Component child);//返回某个索引对应的组件对象public abstract Component GetComponent(int index);}//Class_end
}
2.2.2、叶子对象的实现
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace CompositePattern.CompositeDemo
{/// <summary>/// 叶子对象/// </summary>internal class Leaf : Component{//叶子对象名称private string name=string.Empty;public Leaf(string name){this.name = name;}public override void Add(Component child){throw new NotImplementedException();}public override Component GetComponent(int index){throw new NotImplementedException();}/// <summary>/// 输出叶子对象结构【由于叶子对象没有子对象即只用输出叶子对象的名称】/// </summary>/// <param name="preStr">前缀,主要是按照层级拼接的</param>public override void PrintStruct(string preStr){Console.WriteLine($"{preStr}-{name}");}public override void Remove(Component child){throw new NotImplementedException();}}//Class_end
}
2.2.3、组合对象的实现
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace CompositePattern.CompositeDemo
{/// <summary>/// 组合对象【包含其他组合对象、叶子对象】/// </summary>internal class Composite:Component{//组合对象名称private string name = string.Empty;//用来存储组合对象中包含的子组件对象private List<Component> componentList = null;public Composite(string name){this.name = name;}public override void Add(Component child){if (componentList==null){componentList=new List<Component>();}componentList.Add(child);}public override Component GetComponent(int index){throw new NotImplementedException();}public override void PrintStruct(string preStr){Console.WriteLine($"{preStr}+{name}");if (componentList!=null && componentList.Count>0){preStr += " ";foreach (Component child in componentList){child.PrintStruct(preStr);}}}public override void Remove(Component child){}}//Class_end
}
2.2.4、编写客户端测试
using System.Numerics;namespace CompositePattern
{internal class Program{static void Main(string[] args){CompositeDemoTest();Console.ReadLine();}/// <summary>/// 测试组合示例/// </summary>private static void CompositeDemoTest(){CompositeDemo.Component root = new CompositeDemo.Composite("服装");CompositeDemo.Component c1 = new CompositeDemo.Composite("男装");CompositeDemo.Component c2 = new CompositeDemo.Composite("女装");CompositeDemo.Leaf leaf1 = new CompositeDemo.Leaf("衬衣");CompositeDemo.Leaf leaf2 = new CompositeDemo.Leaf("夹克");CompositeDemo.Leaf leaf3 = new CompositeDemo.Leaf("裙子");CompositeDemo.Leaf leaf4 = new CompositeDemo.Leaf("卫衣");CompositeDemo.Leaf leaf5 = new CompositeDemo.Leaf("牛仔裤");root.Add(c1);root.Add(c2);c1.Add(leaf1);c1.Add(leaf2);c1.Add(leaf3);c2.Add(leaf4);c2.Add(leaf5);root.PrintStruct("");}}//Class_end
}
2.2.5、运行结果
通过这个示例可以看出,通过组合模式,把一个“部分---整体”的层次结构表示成了对象树的结构。这样一来,客户端就无需区分操作的是组合对象还是叶子对象了,对于客户端来说,操作的都是组件对象。
2.2.6、算法中的递归
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;namespace CompositePattern.CompositeDemo
{/// <summary>/// 递归测试/// </summary>internal class RecursiveTest{//递归算法求阶乘【简单示意】public BigInteger Recursive(BigInteger num){if (num == 1){return 1;}return num * Recursive(num-1);}}//Class_end
}/// <summary>
/// 阶乘测试
/// </summary>
private static void RecursiveTestDemo()
{CompositeDemo.RecursiveTest recursiveTest= new CompositeDemo.RecursiveTest();int number = 100;BigInteger res = recursiveTest.Recursive(number);Console.WriteLine($"【{number}】的阶乘是【{res}】");
}
运行结果:
2.3、父组件引用示例
前面的示例中,都是父组件对象中,保存有子组件的引用(即:都是父到子的引用);我们这里实现子组件到父组件对象的引用:
《1》现在需要删除某个商品类别(若该类别没有子类别的话,直接删除就可以了;但若该类别还有子类别,这就涉及到它的子类别处理【一种情况是连带全部删除;一种是上移一层,把被删除的商品类别对象的父商品类别设置为被删除的商品列表的子类别的父商品类别】)。
《2》需要对商品类别细化和调整(把原本属于A类别的商品类别,调整到B类别里面去,某个商品类别的调整会伴随它所有的子类别一起调整。这样的调整可能会把原本是兄弟关系的商品类别变为父子关系,也可能把父子关系调整为兄弟关系,有很多种情况)。
要实现上述的这些功能,一个较为简单的方案就是在保持从父组件到子组件引用的基础上,在增加从子组件到父组件的引用(即:在删除一个组件对象或调整一个组件对象的时候,就可以通过调整父组件的引用来实现,实现起来就简化了复杂性)【通常情况下我们会在Component中定义对父组件的引用,组合对象和叶子对象都可以继承这个引用】(那什么时候来维护这个引用呢?【较为容易得方法是:在组合对象添加子组件对象的时候,为子组件对象设置父组件的引用,在组合对象删除一个子组件对象的时候,再重新设置相关子组件的父组件引用;可以在组合类Composite中实现,这样所有的子类都可以继承到,维护起来更容易】)。
2.3.1、 实现通用的抽象组件对象
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace CompositePattern.ComponentDemo
{/// <summary>/// 抽象的组件对象/// </summary>internal abstract class Component{//父组件对象private Component parent = null;/// <summary>/// 获取一个组件的父组件对象/// </summary>/// <returns></returns>public Component GetParent(){return parent;}/// <summary>/// 设置一个组件的父组件对象/// </summary>/// <param name="parent">父组件对象</param>public void SetParent(Component parent){this.parent = parent;}//返回某个组件的子组件对象public abstract List<Component> GetChildren();//输出组件自己的名称public abstract void PrintStruct(string preStr);//向组合对象中加入组件对象public abstract void Add(Component child);//从组合对象中移除某个组件对象public abstract void Remove(Component child);//返回某个索引对应的组件对象public abstract Component GetComponent(int index);}//Class_end
}
2.3.2、组合对象的实现
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace CompositePattern.ComponentDemo
{/// <summary>/// 组合对象,通常包含其他组合对象或者叶子对象/// </summary>internal class Composite : Component{//用来存储组合对象中包含的子组件对象private List<Component> componentList = null;//组合对象的名称private string name = string.Empty;/// <summary>/// 构造函数/// </summary>/// <param name="name">组合对象的名称</param>public Composite(string name){this.name = name;}public override void Add(Component child){//延迟初始化if (componentList==null){componentList = new List<Component>();}componentList.Add(child);//添加对父组件的引用child.SetParent(this);}public override List<Component> GetChildren(){return componentList;}public override Component GetComponent(int index){if (componentList != null){return componentList[index];}return null;}/// <summary>/// 输出组合对象自身的结构/// </summary>/// <param name="preStr">前缀(主要是按照层级拼接的空格,实现向后缩进)</param>public override void PrintStruct(string preStr){//先把自己输出Console.WriteLine($"{preStr}+{name}");//如果包含子对象就输出if (componentList!=null){preStr += " ";foreach (Component child in componentList){//递归输出每个子对象child.PrintStruct(preStr);}}}public override void Remove(Component child){if (componentList != null){//查找需要删除的组件在集合中的索引位置int index=componentList.IndexOf(child);if (index!=-1){//先把被删除商品类别对象的父商品类别,设置为被删除的商品类别的子类型的父商品类别foreach (var item in child.GetChildren()){//删除的组件对象是本实例的一个子组件对象item.SetParent(this);//把被删除的商品类别对象的子组件对象添加到当前实例中componentList.Add(item);}//真正移除componentList.RemoveAt(index);}}}}//Class_end
}
2.3.3、叶子对象的实现
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace CompositePattern.ComponentDemo
{/// <summary>/// 叶子对象/// </summary>internal class Leaf:Component{//叶子对象名称private string name = string.Empty;/// <summary>/// 构造函数/// </summary>/// <param name="name">叶子对象名称</param>public Leaf(string name){this.name = name;}public override void Add(Component child){throw new NotImplementedException();}public override List<Component> GetChildren(){throw new NotImplementedException();}public override Component GetComponent(int index){throw new NotImplementedException();}/// <summary>/// 输出叶子对象结构【叶子对象没有子对象直接输出叶子对象名称】/// </summary>/// <param name="preStr">前缀,主要是按照层级拼接的空格向后缩进</param>public override void PrintStruct(string preStr){Console.WriteLine($"{preStr}-{name}");}public override void Remove(Component child){throw new NotImplementedException();}}//Class_end
}
2.3.4、客户端测试
using System.Numerics;namespace CompositePattern
{internal class Program{static void Main(string[] args){ComponentRefDemoTest();Console.ReadLine();}/// <summary>/// 组件引用测试/// </summary>private static void ComponentRefDemoTest(){//定义所有的组合对象ComponentDemo.Component root = new ComponentDemo.Composite("服装");ComponentDemo.Component c1 = new ComponentDemo.Composite("男装");ComponentDemo.Component c2 = new ComponentDemo.Composite("女装");//定义所有的叶子对象ComponentDemo.Leaf leaf1 = new ComponentDemo.Leaf("衬衣");ComponentDemo.Leaf leaf2 = new ComponentDemo.Leaf("夹克");ComponentDemo.Leaf leaf3 = new ComponentDemo.Leaf("裙子");ComponentDemo.Leaf leaf4 = new ComponentDemo.Leaf("卫衣");ComponentDemo.Leaf leaf5 = new ComponentDemo.Leaf("牛仔裤");//按照树结构组合对象的叶子对象root.Add(c1);root.Add(c2);c1.Add(leaf1);c1.Add(leaf2);c1.Add(leaf3);c2.Add(leaf4);c2.Add(leaf5);//输出整棵树root.PrintStruct("");Console.WriteLine("----------------");//删除一个节点root.Remove(c1);root.PrintStruct("");}}//Class_end
}
2.3.5、 运行结果
从运行结果可以看到,当男装的节点被删除后,会把原来男装节点下的所有子节点,添加到原来男装的父节点【服装】下面。输出是按照添加的先后顺序来的,所以先输出了女装节点,然后才输出衬衣和夹克节点。
2.4、环状引用示例
环状引用,指的是在对象结构中,某个对象包含的子对象或是子对象的子对象,或是子对象的子对象的子对象......经过N层后,出现所包含的子对象中有这个对象本身,从而构成了环状引用(如:A包含B,B包含C,C又包含A)。
在使用组合模式构建树状结构的时候,这种引用是需要考虑的一种情况,通常情况下,组合模式构建的树状结构,是不应该出现环状引用的,如果出现,多半是有错误发生了;因此在应用组合模式实现功能的时候,就应该考虑要检测并避免出现环状引用,否则容易引起死循环,或是同一个功能被操作多次。
如何检测是否有环状引用的情况发生呢?(一个很简单的思路就是记录下每个组件从根节点开始的路径,因为要出现环状引用,在一条路径上,某个对象必然会出现两次;因此每个对象在整个路径上只是出现一次,那么就不会出现环状引用。这个判断的功能可以添加大Compoistte对象的添加子组件方法中,如果是环状引用的话,就抛出例外,并不会把它加入到子组件中去)。
2.4.1、实现抽象组件对象
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace CompositePattern.LoopRefrence
{/// <summary>/// 抽象的组件对象/// </summary>internal abstract class Component{//记录每个组件的路径private string componentPath = string.Empty;//获取组件的路径public virtual string GetComponentPath { get { return componentPath; } }//设置组件的路径public virtual void SetComponentPath(string componentPath){this.componentPath = componentPath;}//获取组件名称public abstract string GetName();//记录父组件对象private Component parent = null;//获取组件的父组件对象public Component GetParent() { return parent; }//设置组件的父组件对象public void SetParent(Component parent) { this.parent = parent; }//获取某个组件的所有子组件对象public abstract List<Component> GetAllChildren();//输出组件自己的名称public abstract void PrintStruct(string preStr);//向组合对象中加入组件对象public abstract void Add(Component child);//从组合对象中移除某个组件对象public abstract void Remove(Component child);//返回某个索引对应的组件对象public abstract Component GetChild(int index);}//Class_end
}
2.4.2、组合对象的实现
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace CompositePattern.LoopRefrence
{internal class Composite : Component{//用来存储组合对象中包含的子组件对象private List<Component> componentList = null;//组合对象的名称private string name = string.Empty;/// <summary>/// 构造函数/// </summary>/// <param name="name">组合对象的名称</param>public Composite(string name){this.name = name; }public override void Add(Component child){if (componentList == null){componentList = new List<Component>();}componentList.Add(child);//判断组件路径是否为空(若为空则本组件是根组件)if (string.IsNullOrEmpty(this.GetComponentPath) || this.GetComponentPath.Trim().Length==0){//把本组件的name设置大组件路径中this.SetComponentPath(this.name);}//判断要加入的组件在路径上是否出现过if (this.GetComponentPath.StartsWith(child.GetName() + ".")){//说明是根组件,重复添加了throw new Exception($"在本通路【{this.GetComponentPath}】上,组件【{child.GetName()}】已经被添加过了");}else{if (this.GetComponentPath.IndexOf("." + child.GetName()) <= 0){//表示没有出现过,可以添加//计算组件的路径string componentPath = $"{this.GetComponentPath}.{child.GetName()}";//设置子组件的路径child.SetComponentPath(componentPath);}else{throw new Exception($"在本通路【{this.GetComponentPath}】上,组件【{child.GetName()}】已经被添加过了");}}}public override List<Component> GetAllChildren(){return componentList;}public override Component GetChild(int index){return componentList[index];}public override string GetName(){return this.name;}public override void PrintStruct(string preStr){Console.WriteLine($"{preStr}+{name}");if (componentList!=null){preStr += " ";foreach (Component child in componentList){child.PrintStruct(preStr);}}}public override void Remove(Component child){if (componentList!=null){int index = componentList.IndexOf(child);if (index!=-1){//先把被删除的商品类别对象的父商品类别,设置为被删除商品类别的子类别的父商品类别foreach (var item in child.GetAllChildren()){//删除的组件对象是本实例的一个子组件对象item.SetParent(this);componentList.Add(item);}//真正删除componentList.RemoveAt(index);}}}}//Class_end
}
2.4.3、叶子对象的实现
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace CompositePattern.LoopRefrence
{/// <summary>/// 叶子对象/// </summary>internal class Leaf : Component{//叶子对象名称private string name = string.Empty;/// <summary>/// 构造函数/// </summary>/// <param name="name">叶子对象名称</param>public Leaf(string name){this.name = name;}public override void Add(Component child){throw new NotImplementedException();}public override List<Component> GetAllChildren(){throw new NotImplementedException();}public override Component GetChild(int index){throw new NotImplementedException();}public override string GetName(){return this.name;}/// <summary>/// 输出叶子对象的结构【由于叶子对象没有子对象,也就是说可以直接输出对象名称】/// </summary>/// <param name="preStr">前缀,主要是按照层级拼接的空格,向后缩进</param>public override void PrintStruct(string preStr){Console.WriteLine($"{preStr}-{name}");}public override void Remove(Component child){throw new NotImplementedException();}}//Class_end
}
2.4.4、客户端测试
using System.Numerics;namespace CompositePattern
{internal class Program{static void Main(string[] args){LoopRefrenceTest();Console.ReadLine();}/// <summary>/// 循环引用测试/// </summary>private static void LoopRefrenceTest(){//定义所有组合对象LoopRefrence.Component root = new LoopRefrence.Composite("服装");LoopRefrence.Component c1 = new LoopRefrence.Composite("男装");LoopRefrence.Component c2 = new LoopRefrence.Composite("女装");LoopRefrence.Component c3 = new LoopRefrence.Composite("男装");//设置一个环状引用root.Add(c1);c1.Add(c2);c2.Add(c3);root.PrintStruct("");}}//Class_end
}
2.4.5、运行结果
如上实现的进行环路检测的实现是很简单的;但还是有一些问题没有考虑(如:要是删除了路径上的某个组件对象,那么所有该组件对象的子组件对象所路径的路径,都需要修改,要把这个组件从所有相关路径上都删除。就是在被删除的组件对象的所有子组件对象的路径上,查找被删除组件的名称,然后通过字符串截取的方式将其删除。但是这样的实现方式不好。要实现这样的功能,可以考虑使用动态计算路径的方式,每次添加一个组件的时候,动态地递归寻找父组件,然后父组件在找父组件,一直到根组件,这样就能避免某个组件被删除后,路径发生变化,需要修改所有相关路径的情况)。
三、项目源码工程
kafeiweimei/Learning_DesignPattern: 这是一个关于C#语言编写的基础设计模式项目工程,方便学习理解常见的26种设计模式https://github.com/kafeiweimei/Learning_DesignPattern