本文摘取自TerryLee(李会军)老师的设计模式系列文章,版权归TerryLee,仅供个人学习参考。转载请标明原作者TerryLee。部分示例代码来自DoFactory。
概述
组合模式有时候又叫做部分-整体模式,它使我们树型结构的问题中,模糊了简单元素和复杂元素的概念,客户程序可以向处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素的内部结构解耦。
意图
将对象组合成树形结构以表示"部分-整体"的层次结构。Composite模式使得用户对单个对象和组合对象的使用具有一致性。
UML
图1 Composite模式结构图
参与者
这个模式涉及的类或对象:
-
Component
-
声明对象及组合对象的接口
-
视情况而定,给所有类实现的公共接口添加默认行为。
-
定义访问与管理子对象的接口。
-
(可选的)定义访问递归结构中组件父对象的接口,并在适当情况下给出实现。
-
Leaf
-
表示组合中的叶子对象。叶子对象没有子元素。
-
定义组合中元对象的行为。
-
Composite
-
定义拥有子对象组合对象的行为。
-
存储子对象。
-
实现Component接口中子对象相关的操作。
-
Client
-
通过组合接口操作组合体中的对象。
适用性
组合模式是一个内存中的数据结构,表示一组对象,其中每一项是独立的对象或另一组对象。树控件是组合模式一个很好的例子。树的节点或者包含一个独立的对象(叶子节点)或者包含一组对象(一个子树)。组合模式中所有节点共享一个公共的接口,该接口即支持独立的项也支持一组项的操作。这个公共接口极大的方便了迭代组合集合中每一项的递归算法的设计与构造。
从根本上说,组合模式是一个用于构造树及导向图的集合。它的使用如同其它集合,数组,列表,栈,字典等。
以下情况下适用Composite模式:
-
你想表示对象的部分-整体层次结构。
-
你希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。
DoFactory GoF代码
这个例子中创建了一个树结构,每一个节点都可以使用统一的方式访问,不管其实叶子节点或是分支节点。
// Composite pattern // Structural example using System; using System.Collections.Generic; namespace DoFactory.GangOfFour.Composite.Structural { // MainApp test application class MainApp { static void Main() { // Create a tree structure Composite root = new Composite("root"); root.Add(new Leaf("Leaf A")); root.Add(new Leaf("Leaf B")); Composite comp = new Composite("Composite X"); comp.Add(new Leaf("Leaf XA")); comp.Add(new Leaf("Leaf XB")); root.Add(comp); root.Add(new Leaf("Leaf C")); // Add and remove a leaf Leaf leaf = new Leaf("Leaf D"); root.Add(leaf); root.Remove(leaf); // Recursively display tree root.Display(1); // Wait for user Console.ReadKey(); } } // "Component" abstract class Component { protected string name; // Constructor public Component(string name) { this.name = name; } public abstract void Add(Component c); public abstract void Remove(Component c); public abstract void Display(int depth); } // "Composite" class Composite : Component { private List<Component> _children = new List<Component>(); // Constructor public Composite(string name) : base(name) { } public override void Add(Component component) { _children.Add(component); } public override void Remove(Component component) { _children.Remove(component); } public override void Display(int depth) { Console.WriteLine(new String('-', depth) + name); // Recursively display child nodes foreach (Component component in _children) { component.Display(depth + 2); } } } // "Leaf" class Leaf : Component { // Constructor public Leaf(string name) : base(name) { } public override void Add(Component c) { Console.WriteLine("Cannot add to a leaf"); } public override void Remove(Component c) { Console.WriteLine("Cannot remove from a leaf"); } public override void Display(int depth) { Console.WriteLine(new String('-', depth) + name); } } }
这个例子展示了使用组合模式构建一个表示图形对象的树结构,其由基础对象(线,圆形等)和组合对象(一组基础元素组成的更复杂的元素)。
例子中涉及到的类与组合模式中标准的类对应关系如下:
-
Component – DrawingElement
-
Leaf – PrimitiveElement
-
Composite – CompositeElement
-
Client - CompositeApp
// Composite pattern // Real World example using System; using System.Collections.Generic; namespace DoFactory.GangOfFour.Composite.RealWorld { // Mainapp test application class MainApp { static void Main() { // Create a tree structure CompositeElement root = new CompositeElement("Picture"); root.Add(new PrimitiveElement("Red Line")); root.Add(new PrimitiveElement("Blue Circle")); root.Add(new PrimitiveElement("Green Box")); // Create a branch CompositeElement comp = new CompositeElement("Two Circles"); comp.Add(new PrimitiveElement("Black Circle")); comp.Add(new PrimitiveElement("White Circle")); root.Add(comp); // Add and remove a PrimitiveElement PrimitiveElement pe = new PrimitiveElement("Yellow Line"); root.Add(pe); root.Remove(pe); // Recursively display nodes root.Display(1); // Wait for user Console.ReadKey(); } } // "Component" Treenode abstract class DrawingElement { protected string _name; // Constructor public DrawingElement(string name) { this._name = name; } public abstract void Add(DrawingElement d); public abstract void Remove(DrawingElement d); public abstract void Display(int indent); } // "Leaf" class PrimitiveElement : DrawingElement { // Constructor public PrimitiveElement(string name): base(name) { } public override void Add(DrawingElement c) { Console.WriteLine("Cannot add to a PrimitiveElement"); } public override void Remove(DrawingElement c) { Console.WriteLine("Cannot remove from a PrimitiveElement"); } public override void Display(int indent) { Console.WriteLine(new String('-', indent) + " " + _name); } } // "Composite" class CompositeElement : DrawingElement { private List<DrawingElement> elements = new List<DrawingElement>(); // Constructor public CompositeElement(string name): base(name) { } public override void Add(DrawingElement d) { elements.Add(d); } public override void Remove(DrawingElement d) { elements.Remove(d); } public override void Display(int indent) { Console.WriteLine(new String('-', indent) + "+ " + _name); // Display each child element on this node foreach (DrawingElement d in elements) { d.Display(indent + 2); } } } }
在.NET优化版本的示例中,泛型的使用贯穿始终。例子中创建了一个名为TreeNode<T>的泛型类。这是一个开放的类型,其可以接受任意类型参数。但TreeNode也添加了一个泛型约束,即类型T必须实现IComparable<T>接口。名为Shape的类实现了这个泛型接口,所以两个Shape对象可以进行比较。这个泛型类极大方便了由树节点列表添加或移除Shape对象的过程。这段代码展示了泛型提供给.NET开发者的巨大能力。另外这个例子中也用到一些列有趣的语言特性,包括泛型,自动属性及递归(ForEach扩展方法)。
// Composite pattern // .NET Optimized example using System; using System.Collections.Generic; namespace DoFactory.GangOfFour.Composite.NETOptimized { class MainApp { static void Main() { // Build a tree of shapes var root = new TreeNode<Shape> { Node = new Shape("Picture") }; root.Add(new Shape("Red Line")); root.Add(new Shape("Blue Circle")); root.Add(new Shape("Green Box")); var branch = root.Add(new Shape("Two Circles")); branch.Add(new Shape("Black Circle")); branch.Add(new Shape("White Circle")); // Add, remove, and add a shape var shape = new Shape("Yellow Line"); root.Add(shape); root.Remove(shape); root.Add(shape); // Display tree using static method TreeNode<Shape>.Display(root, 1); Console.ReadKey(); } } // "Generic tree node " class TreeNode<T> where T : IComparable<T> { private List<TreeNode<T>> _children = new List<TreeNode<T>>(); // Add a child tree node public TreeNode<T> Add(T child) { var newNode = new TreeNode<T> { Node = child }; _children.Add(newNode); return newNode; } // Remove a child tree node public void Remove(T child) { foreach (var treeNode in _children) { if (treeNode.Node.CompareTo(child) == 0) { _children.Remove(treeNode); return; } } } // Gets or sets the node public T Node { get; set; } // Gets treenode children public List<TreeNode<T>> Children { get { return _children; } } // Recursively displays node and its children public static void Display(TreeNode<T> node, int indentation) { string line = new String('-', indentation); Console.WriteLine(line + " " + node.Node); node.Children.ForEach(n => Display(n, indentation + 1)); } } // "Shape" // Implements generic IComparable interface class Shape : IComparable<Shape> { private string _name; // Constructor public Shape(string name) { this._name = name; } public override string ToString() { return _name; } // IComparable<Shape> Member public int CompareTo(Shape other) { return (this == other) ? 0 : -1; } } }
组合模式解说
这里我们用绘图这个例子来说明Composite模式,通过一些基本图像元素(直线、圆等)以及一些复合图像元素(由基本图像元素组合而成)构建复杂的图形树。在设计中我们对每一个对象都配备一个Draw()方法,在调用时,会显示相关的图形。可以看到,这里复合图像元素它在充当对象的同时,又是那些基本图像元素的一个容器。先看一下基本的类结构图:
图2.绘图程序的基本结构
图中橙色的区域表示的是复合图像元素。示意性代码:
public abstract class Graphics { protected string _name; public Graphics(string name) { this._name = name; } public abstract void Draw(); } public class Picture : Graphics { public Picture(string name) : base(name) { } public override void Draw() { // ... ... } public ArrayList GetChilds() { // ... 返回所有的子对象 } }
而其他作为树枝构件,实现代码如下:
public class Line : Graphics { public Line(string name) : base(name) { } public override void Draw() { Console.WriteLine("Draw a" + _name.ToString()); } } public class Circle : Graphics { public Circle(string name) : base(name) { } public override void Draw() { Console.WriteLine("Draw a" + _name.ToString()); } } public class Rectangle : Graphics { public Rectangle(string name) : base(name) { } public override void Draw() { Console.WriteLine("Draw a" + _name.ToString()); } }
现在我们要对该图像元素进行处理:在客户端程序中,需要判断返回对象的具体类型到底是基本图像元素,还是复合图像元素。如果是复合图像元素,我们将要用递归去处理,然而这种处理的结果却增加了客户端程序与复杂图像元素内部结构之间的依赖,那么我们如何去解耦这种关系呢?我们希望的是客户程序可以像处理基本图像元素一样来处理复合图像元素,这就要引入Composite模式了,需要把对于子对象的管理工作交给复合图像元素,为了进行子对象的管理,它必须提供必要的Add(),Remove()等方法,类结构图如下:
图3. 实现了子对象概念的绘图程序的结构图
示意性代码:
public abstract class Graphics { protected string _name; public Graphics(string name) { this._name = name; } public abstract void Draw(); public abstract void Add(); public abstract void Remove(); } public class Picture : Graphics { protected ArrayList picList = new ArrayList(); public Picture(string name) : base(name) { } public override void Draw() { Console.WriteLine("Draw a" + _name.ToString()); foreach (Graphics g in picList) { g.Draw(); } } public override void Add(Graphics g) { picList.Add(g); } public override void Remove(Graphics g) { picList.Remove(g); } } public class Line : Graphics { public Line(string name) : base(name) { } public override void Draw() { Console.WriteLine("Draw a" + _name.ToString()); } public override void Add(Graphics g) { } public override void Remove(Graphics g) { } } public class Circle : Graphics { public Circle(string name) : base(name) { } public override void Draw() { Console.WriteLine("Draw a" + _name.ToString()); } public override void Add(Graphics g) { } public override void Remove(Graphics g) { } } public class Rectangle : Graphics { public Rectangle(string name) : base(name) { } public override void Draw() { Console.WriteLine("Draw a" + _name.ToString()); } public override void Add(Graphics g) { } public override void Remove(Graphics g) { } }
这样引入Composite模式后,客户端程序不再依赖于复合图像元素的内部实现了。然而,我们程序中仍然存在着问题,因为Line,Rectangle,Circle已经没有了子对象,它是一个基本图像元素,因此Add(),Remove()的方法对于它来说没有任何意义,而且把这种错误不会在编译的时候报错,把错误放在了运行期,我们希望能够捕获到这类错误,并加以处理,稍微改进一下我们的程序:
public class Line : Graphics { public Line(string name) : base(name) { } public override void Draw() { Console.WriteLine("Draw a" + _name.ToString()); } public override void Add(Graphics g) { //抛出一个我们自定义的异常 } public override void Remove(Graphics g) { //抛出一个我们自定义的异常 } }
这样改进以后,我们可以捕获可能出现的错误,做进一步的处理。上面的这种实现方法属于透明式的Composite模式,如果我们想要更安全的一种做法,就需要把管理子对象的方法声明在树枝构件Picture类里面,这样如果叶子节点Line,Rectangle,Circle使用这些方法时,在编译期就会出错,看一下类结构图:
图4. 使用Composite模式实现的绘图程序结构图
示意性代码:
public abstract class Graphics { protected string _name; public Graphics(string name) { this._name = name; } public abstract void Draw(); } public class Picture : Graphics { protected ArrayList picList = new ArrayList(); public Picture(string name) : base(name) { } public override void Draw() { Console.WriteLine("Draw a" + _name.ToString()); foreach (Graphics g in picList) { g.Draw(); } } public void Add(Graphics g) { picList.Add(g); } public void Remove(Graphics g) { picList.Remove(g); } } public class Line : Graphics { public Line(string name) : base(name) { } public override void Draw() { Console.WriteLine("Draw a" + _name.ToString()); } } public class Circle : Graphics { public Circle(string name) : base(name) { } public override void Draw() { Console.WriteLine("Draw a" + _name.ToString()); } } public class Rectangle : Graphics { public Rectangle(string name) : base(name) { } public override void Draw() { Console.WriteLine("Draw a" + _name.ToString()); } }
这种方式属于安全式的Composite模式,在这种方式下,虽然避免了前面所讨论的错误,但是它也使得叶子节点和树枝构件具有不一样的接口。这种方式和透明式的Composite各有优劣,具体使用哪一个,需要根据问题的实际情况而定。通过Composite模式,客户程序在调用Draw()的时候不用再去判断复杂图像元素中的子对象到底是基本图像元素,还是复杂图像元素,看一下简单的客户端调用:
public class App { public static void Main() { Picture root = new Picture("Root"); root.Add(new Line("Line")); root.Add(new Circle("Circle")); Rectangle r = new Rectangle("Rectangle"); root.Add(r); root.Draw(); } }
.NET中的组合模式
如果有人用过Enterprise Library2.0,一定在源程序中看到了一个叫做ObjectBuilder的程序集,顾名思义,它是用来负责对象的创建工作的,而在ObjectBuilder中,有一个被称为定位器的东西,通过定位器,可以很容易的找到对象,它的结构采用链表结构,每一个节点是一个键值对,用来标识对象的唯一性,使得对象不会被重复创建。定位器的链表结构采用可枚举的接口类来实现,这样我们可以通过一个迭代器来遍历这个链表。同时多个定位器也被串成一个链表。具体地说就是多个定位器组成一个链表,表中的每一个节点是一个定位器,定位器本身又是一个链表,表中保存着多个由键值对组成的对象的节点。所以这是一个典型的Composite模式的例子,来看它的结构图:
图5.使用了Composite模式的定位器结构图
正如我们在图中所看到的,IReadableLocator定义了最上层的定位器接口方法,它基本上具备了定位器的大部分功能。部分代码:
public interface IReadableLocator : IEnumerable<KeyValuePair<object, object>> { //返回定位器中节点的数量 int Count { get; } //一个指向父节点的引用 IReadableLocator ParentLocator { get; } //表示定位器是否只读 bool ReadOnly { get; } //查询定位器中是否已经存在指定键值的对象 bool Contains(object key); //查询定位器中是否已经存在指定键值的对象,根据给出的搜索选项,表示是否要向上回溯继续寻找。 bool Contains(object key, SearchMode options); //使用谓词操作来查找包含给定对象的定位器 IReadableLocator FindBy(Predicate<KeyValuePair<object, object>> predicate); //根据是否回溯的选项,使用谓词操作来查找包含对象的定位器 IReadableLocator FindBy(SearchMode options, Predicate<KeyValuePair<object, object>> predicate); //从定位器中获取一个指定类型的对象 TItem Get<TItem>(); //从定位其中获取一个指定键值的对象 TItem Get<TItem>(object key); //根据选项条件,从定位其中获取一个指定类型的对象 TItem Get<TItem>(object key, SearchMode options); //给定对象键值获取对象的非泛型重载方法 object Get(object key); //给定对象键值带搜索条件的非泛型重载方法 object Get(object key, SearchMode options); } 一个抽象基类ReadableLocator用来实现这个接口的公共方法。两个主要的方法实现代码如下: public abstract class ReadableLocator : IReadableLocator { /// <summary> /// 查找定位器,最后返回一个只读定位器的实例 /// </summary> public IReadableLocator FindBy(SearchMode options, Predicate<KeyValuePair<object, object>> predicate) { if (predicate == null) throw new ArgumentNullException("predicate"); if (!Enum.IsDefined(typeof(SearchMode), options)) throw new ArgumentException(Properties.Resources.InvalidEnumerationValue, "options"); Locator results = new Locator(); IReadableLocator currentLocator = this; while (currentLocator != null) { FindInLocator(predicate, results, currentLocator); currentLocator = options == SearchMode.Local ? null : currentLocator.ParentLocator; } return new ReadOnlyLocator(results); } /// <summary> /// 遍历定位器 /// </summary> private void FindInLocator(Predicate<KeyValuePair<object, object>> predicate, Locator results, IReadableLocator currentLocator) { foreach (KeyValuePair<object, object> kvp in currentLocator) { if (!results.Contains(kvp.Key) && predicate(kvp)) { results.Add(kvp.Key, kvp.Value); } } } }
可以看到,在FindBy方法里面,循环调用了FindInLocator方法,如果查询选项是只查找当前定位器,那么循环终止,否则沿着定位器的父定位器继续向上查找。FindInLocator方法就是遍历定位器,然后把找到的对象存入一个临时的定位器。最后返回一个只读定位器的新的实例。
从这个抽象基类中派生出一个具体类和一个抽象类,一个具体类是只读定位器(ReadOnlyLocator),只读定位器实现抽象基类没有实现的方法,它封装了一个实现了IReadableLocator接口的定位器,然后屏蔽内部定位器的写入接口方法。另一个继承的是读写定位器抽象类ReadWriteLocator,为了实现对定位器的写入和删除,这里定义了一个对IReadableLocator接口扩展的接口叫做IReadWriteLocator,在这个接口里面提供了实现定位器的操作:
图6.例子中组合模式中的接口的结构图
实现代码如下:
public interface IReadWriteLocator : IReadableLocator { //保存对象到定位器 void Add(object key, object value); //从定位器中删除一个对象,如果成功返回真,否则返回假 bool Remove(object key); }
从ReadWirteLocator派生的具体类是Locator类,Locator类必须实现一个定位器的全部功能,现在我们所看到的Locator它已经具有了管理定位器的功能,同时他还应该具有存储的结构,这个结构是通过一个WeakRefDictionary类来实现的,这里就不介绍了。
组合模式在.NET中广泛使用。Windows Form应用(System.Windows.Forms命名空间)与ASP.NET(System.Web.UI命名空间)中的Control类是两个例子。Control类支持的操作可以应用于所有的控件及它们的包含的控件,也支持一些处理子控件用的操作(如返回子控件集合的Controls属性)。内置的TreeNode类是.NET Framework中另一个组合模式的例子。WPF同样有很多内置的控件使用了组合模式。
来自《大话设计模式》的例子
这个例子使用组合模式实现了一个大型公司的组织结构。总公司下有办事部门及一些子公司,每个子公司也有自己的部门。这个例子的UML如下:
图7. 公司组织架构示例的UML图
using System; using System.Collections.Generic; using System.Text; namespace 组合模式 { class Program { static void Main(string[] args) { ConcreteCompany root = new ConcreteCompany("北京总公司"); root.Add(new HRDepartment("总公司人力资源部")); root.Add(new FinanceDepartment("总公司财务部")); ConcreteCompany comp = new ConcreteCompany("上海华东分公司"); comp.Add(new HRDepartment("华东分公司人力资源部")); comp.Add(new FinanceDepartment("华东分公司财务部")); root.Add(comp); ConcreteCompany comp1 = new ConcreteCompany("南京办事处"); comp1.Add(new HRDepartment("南京办事处人力资源部")); comp1.Add(new FinanceDepartment("南京办事处财务部")); comp.Add(comp1); ConcreteCompany comp2 = new ConcreteCompany("杭州办事处"); comp2.Add(new HRDepartment("杭州办事处人力资源部")); comp2.Add(new FinanceDepartment("杭州办事处财务部")); comp.Add(comp2); Console.WriteLine(" 结构图:"); root.Display(1); Console.WriteLine(" 职责:"); root.LineOfDuty(); Console.Read(); } } abstract class Company { protected string name; public Company(string name) { this.name = name; } public abstract void Add(Company c);//增加 public abstract void Remove(Company c);//移除 public abstract void Display(int depth);//显示 public abstract void LineOfDuty();//履行职责 } class ConcreteCompany : Company { private List<Company> children = new List<Company>(); public ConcreteCompany(string name) : base(name) { } public override void Add(Company c) { children.Add(c); } public override void Remove(Company c) { children.Remove(c); } public override void Display(int depth) { Console.WriteLine(new String('-', depth) + name); foreach (Company component in children) { component.Display(depth + 2); } } //履行职责 public override void LineOfDuty() { foreach (Company component in children) { component.LineOfDuty(); } } } //人力资源部 class HRDepartment : Company { public HRDepartment(string name) : base(name) { } public override void Add(Company c) { } public override void Remove(Company c) { } public override void Display(int depth) { Console.WriteLine(new String('-', depth) + name); } public override void LineOfDuty() { Console.WriteLine("{0} 员工招聘培训管理", name); } } //财务部 class FinanceDepartment : Company { public FinanceDepartment(string name) : base(name) { } public override void Add(Company c) { } public override void Remove(Company c) { } public override void Display(int depth) { Console.WriteLine(new String('-', depth) + name); } public override void LineOfDuty() { Console.WriteLine("{0} 公司财务收支管理", name); } } }
效果及实现要点
-
Component – DrawingElement
-
Leaf – PrimitiveElement
-
Composite – CompositeElement
-
Client - CompositeApp
-
Composite模式采用树形结构来实现普遍存在的对象容器,从而将"一对多"的关系转化"一对一"的关系,使得客户代码可以一致地处理对象和对象容器,无需关心处理的是单个的对象,还是组合的对象容器。
-
将"客户代码与复杂的对象容器结构"解耦是Composite模式的核心思想,解耦之后,客户代码将与纯粹的抽象接口——而非对象容器的复内部实现结构——发生依赖关系,从而更能"应对变化"。
-
Composite模式中,是将"Add和Remove等和对象容器相关的方法"定义在"表示抽象对象的Component类"中,还是将其定义在"表示对象容器的Composite类"中,是一个关乎"透明性"和"安全性"的两难问题,需要仔细权衡。这里有可能违背面向对象的"单一职责原则",但是对于这种特殊结构,这又是必须付出的代价。ASP.NET控件的实现在这方面为我们提供了一个很好的示范。
-
Composite模式在具体实现中,可以让父对象中的子对象反向追溯;如果父对象有频繁的遍历需求,可使用缓存技巧来改善效率。
组合模式是一种单一职责原则与透明性的权衡(即前文谈到的透明方式与安全方式),为了使叶节点与组合节点在客户端用起来方式一致,叶子结点继承了组合节点的一些功能。这也多少违反了单一职责原则。
总结
组合模式解耦了客户程序与复杂元素内部结构,从而使客户程序可以向处理简单元素一样来处理复杂元素。