概述
组合模式有时候又叫做部分-整体模式,它使我们树型结构的问题中,模糊了简单元素和复杂元素的概念,客户程序可以向处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素的内部结构解耦。
描述Composite模式的最佳方式莫过于树形图。从抽象类或接口为根节点开始,然后生枝发芽,以形成树枝节点和叶结点。因此,Composite模式通常用来描述部分与整体之间的关系,而通过根节点对该结构的抽象,使得客户端可以将单元素节点与复合元素节点作为相同的对象来看待。
意图
将对象组合成树形结构以表示“部分-整体”的层次结构。Composite模式使得用户对单个对象和组合对象的使用具有一致性。[GOF 《设计模式》]
结构图
1.安全式的合成模式的结构
安全式的合成模式要求管理聚集的方法只出现在树枝构件类中,而不出现在树叶构件中。
这种形式涉及到三个角色:
- 抽象构件(Component)角色:这是一个抽象角色,它给参加组合的对象定义出公共的接口及其默认行为,可以用来管理所有的子对象。在安全式的合成模式里,构件角色并不是定义出管理子对象的方法,这一定义由树枝构件对象给出。
- 树叶构件(Leaf)角色:树叶对象是没有下级子对象的对象,定义出参加组合的原始对象的行为。
- 树枝构件(Composite)角色:代表参加组合的有下级子对象的对象。树枝对象给出所有的管理子对象的方法,如add()、remove()、getChild()等。
2.透明式的合成模式结构
与安全式的合成模式不同的是,透明式的合成模式要求所有的具体构件类,不论树枝构件还是树叶构件,均符合一个固定的接口。
这种形式涉及到三个角色:
- 抽象构件(Component)角色:这是一个抽象角色,它给参加组合的对象规定一个接口,规范共有的接口及默认行为。
- 树叶构件(Leaf)角色:代表参加组合的树叶对象,定义出参加组合的原始对象的行为。树叶类会给出add()、remove()以及getChild()之类的用来管理子类对对象的方法的平庸实现。
- 树枝构件(Composite)角色:代表参加组合的有子对象的对象,定义出这样的对象的行为。
生活中的例子
组合模式将对象组合成树形结构以表示"部分-整体"的层次结构。让用户一致地使用单个对象和组合对象。虽然例子抽象一些,但是算术表达式确实是组合的例子。算术表达式包括操作数、操作符和另一个操作数。操作数可以是数字,也可以是另一个表达式。这样,2+3和(2+3)+(4*6)都是合法的表达式。
图2 使用算术表达式例子的Composite模式对象图
示例用例图
建造房子正是一种组合模式,先把房子的结构建起来,再装上窗户和门就是一所房子了.来看用例图:
代码设计
先创建接口IBuild.cs:
public interface IBuild { string Action { get; set; } string Create(); string Finish(); }
再创建Build.cs:
public abstract class Build : IBuild { private string _Action; public string Action { get { return _Action; } set { _Action = value; } } public Build(string action) { this.Action = action; } public abstract string Create(); public abstract string Finish(); }
再创建House.cs:
public class House : Build { protected List<IBuild> houseBuild = new List<IBuild>(); public House(string action) : base(action) { } public override string Create() { StringBuilder strBuilder = new StringBuilder(); strBuilder.AppendFormat("开始建造{0}.\n",Action); foreach (IBuild build in houseBuild) { strBuilder.AppendLine(build.Create()); strBuilder.AppendLine(build.Finish()); } return strBuilder.ToString(); } public void Add(IBuild build) { houseBuild.Add(build); } public void Remove(IBuild build) { houseBuild.Remove(build); } public override string Finish() { return string.Format("{0}建造完成.", Action); } }
再创建Construct.cs:
public class Construct : Build { public Construct(string Action) : base(Action) { } public override string Create() { return string.Format("正在建造{0}", Action); } public override string Finish() { return string.Format("{0}建造完成.", Action); } }
再创建Window.cs:
public class Window : Build { public Window(string Action) : base(Action) { } public override string Create() { return string.Format("正在安装{0}.", Action); } public override string Finish() { return string.Format("{0}建造完成.", Action); } }
再创建Door.cs:
public class Door : Build { public Door(string Action) : base(Action) { } public override string Create() { return string.Format("正在安装{0}", Action); } public override string Finish() { return string.Format("{0}建造完成.", Action); } }
最后调用:
public partial class Run : Form { public Run() { InitializeComponent(); } private void btnRun_Click(object sender, EventArgs e) { //------------------------------------- House buildhouse = new House("房子"); buildhouse.Add(new Construct("房子结构")); buildhouse.Add(new Window("窗户")); buildhouse.Add(new Door("房门")); rtbResult.AppendText(buildhouse.Create()); rtbResult.AppendText(buildhouse.Finish()); } }
结果如图:
效果及实现要点
1.Composite模式采用树形结构来实现普遍存在的对象容器,从而将“一对多”的关系转化“一对一”的关系,使得客户代码可以一致地处理对象和对象容器,无需关心处理的是单个的对象,还是组合的对象容器。
2.将“客户代码与复杂的对象容器结构”解耦是Composite模式的核心思想,解耦之后,客户代码将与纯粹的抽象接口——而非对象容器的复内部实现结构——发生依赖关系,从而更能“应对变化”。
3.Composite模式中,是将“Add和Remove等和对象容器相关的方法”定义在“表示抽象对象的Component类”中,还是将其定义在“表示对象容器的Composite类”中,是一个关乎“透明性”和“安全性”的两难问题,需要仔细权衡。这里有可能违背面向对象的“单一职责原则”,但是对于这种特殊结构,这又是必须付出的代价。ASP.NET控件的实现在这方面为我们提供了一个很好的示范。
4.Composite模式在具体实现中,可以让父对象中的子对象反向追溯;如果父对象有频繁的遍历需求,可使用缓存技巧来改善效率。
5.使用透明模式还是安全模式根据自己的需要定。
6.在某些情况下,树叶构件可以访问树枝构件获取一些信息。
7.如果树叶构件数量比较多,树枝构件频繁遍历子节点的话可以考虑进行缓存。
8.既然所有对象有了统一的接口,客户端应该针对抽象构件进行编程。
适用性
以下情况下适用Composite模式:
1.你想表示对象的部分-整体层次结构
2.你希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。
3.从代码角度来说,如果类型之间组成了层次结构,你希望使用统一的接口来管理每一个层次的类型的时候。
4.从应用角度来说,如果你希望把一对多的关系转化为一对一的关系的时候。
注意事项
1.明显的给出父对象的引用。在子对象里面给出父对象的引用,可以很容易的遍历所有父对象。有了这个引用,可以方便的应用责任链模式。
2.在通常的系统里,可以使用享元模式实现构件的共享,但是由于合成模式的对象经常要有对父对象的引用,因此共享不容易实现。
3.有时候系统需要遍历一个树枝结构的子构件很多次,这时候可以考虑把遍历子构件的结果暂时存储在父构件里面作为缓存。
4.关于使用什么数据类型来存储子对象的问题,在示意性的代码中使用了ArrayList,在实际系统中可以使用其它聚集或数组等。
5.客户端尽量不要直接调用树叶类中的方法,而是借助其父类(Component)的多态性完成调用,这样可以增加代码的复用性。
总结
1.组合模式解耦了客户程序与复杂元素内部结构,从而使客户程序可以向处理简单元素一样来处理复杂元素。