一、模式概述
描述Composite模式的最佳方式莫过于树形图。从抽象类或接口为根节点开始,然后生枝发芽,以形成树枝节点和叶结点。因此,Composite模式通常用来描述部分与整体之间的关系,而通过根节点对该结构的抽象,使得客户端可以将单元素节点与复合元素节点作为相同的对象来看待。
由于Composite模式模糊了单元素和复合元素的区别,就使得我们为这些元素提供相关的操作时,可以有一个统一的接口。例如,我们要编写一个字处理软件,该软件能够处理文字,对文章进行排版、预览、打印等功能。那么,这个工具需要处理的对象,就应该包括单个的文字、以及由文字组成的段落,乃至整篇文档。这些对象从软件处理的角度来看,对外的接口应该是一致的,例如改变文字的字体,改变文字的位置使其居中或者右对齐,也可以显示对象的内容,或者打印。而从内部实现来看,我们对段落或者文档进行操作,实质上也是对文字进行操作。从结构来看,段落包含了文字,文档又包含了段落,是一个典型的树形结构。而其根节点正是我们可以抽象出来暴露在外的统一接口,例如接口IElement:
既然文字、段落、文档都具有这些操作,因此它们都可以实现IElement接口:
从上图可以看到,对象Word、Paragraph、Document均实现了IElement接口,但Paragraph和Document与Word对象不同的是,这两者处除了实现了IElement接口,它们还与IElement接口对象之间具有聚合的关系,且是一对多。也就是说Paragraph与Document对象内可以包含0个到多个IElement对象,这也是与前面对字处理软件分析获得的结果是一致的。
从整个结构来看,完全符合树形结构的各个要素,接口IElement是根节点,而Paragraph和Document类为枝节点,Word对象为叶节点。既然作为枝节点,它就具有带叶节点的能力,从上图的聚合关系中我们体现出了这一点。也就是说,Paragraph和Document类除了具有排版、打印方面的职责外,还能够添加、删除叶节点的操作。那么这些操作应该放在哪里呢?
管理对子节点的管理,Composite模式提供了两种方式:一个是透明方式,也就是说在根节点中声明所有用来管理子元素的方法,包括Add()、Remove()等方法。这样一来,实现根节点接口的子节点同时也具备了管理子元素的能力。这种实现策略,最大的好处就是完全消除了叶节点和枝节点对象在抽象层次的区别,它们具备完全一致的接口。而缺点则是不够安全。由于叶节点本身不具备管理子元素的能力,因此提供的Add()、Remove()方法在实现层次是无意义的。但客户端调用时,却看不到这一点,从而导致在运行期间有出错的可能。
另一种策略则是安全方式。与透明方式刚好相反,它只在枝节点对象里声明管理子元素的方法,由于叶节点不具备这些方法,当客户端在操作叶节点时,就不会出现前一种方式的安全错误。然而,这种实现方式,却导致了叶节点和枝节点接口的不完全一致,这给客户端操作时带来了不便。
这两种方式各有优缺点,我们在实现时,应根据具体的情况,作出更加合理的抉择。在字处理软件一例中,我选择了安全方式来实现,因为对于客户端而言,在调用IElement接口时,通常是将其视为可被排版、打印等操作的对象。至于为Paragraph和Document对象添加、删除子对象,往往是一种初始化的行为,完全可以放到一个单独的模块中。根据单一职责原则(SRP),我们没有必要让IElement接口负累太重。所以,我们需要对上图作稍许的修改,在Paragraph和Document对象中增加Add()和Remove()方法:
以下是IElement对象结构的实现代码:
public interface IElement
{
void ChangeFont(Font font);
void Show();
//其他方法略;
}
public class Word
{
public void ChangeFont(Font font)
{
this.font = font;
}
public void Show()
{
Console.WriteLine(this.ToString());
}
//其他方法略;
}
public class Paragraph
{
private ArrayList elements = new ArrayList();
public void Add(IElement element)
{
elements.Add(element);
}
public void Remove(IElement element)
{
elements.Remove(element);
}
public void ChangeFont(Font font)
{
foreach (IElement element in elements)
{
element.ChangeFont(font);
}
}
public void Show()
{
foreach (IElement element in elements)
{
element.Show(font);
}
}
//其他方法略;
}
//Document类略;
实际上,我们在为叶节点实现Add(),Remove()方法时,还需要考虑一些异常情况。例如在Paragraph类中,添加的子元素就不能是Document对象和Paragraph对象。所以在添加IElement对象时,还需要做一些条件判断,以确定添加行为是否正确,如果错误,应抛出异常。
采用Composite模式,我们将Word、Paragraph、Document抽象为IElement接口。虽然各自内部的实现并不相同,枝节点和叶节点的实质也不一样,但对于调用者而言,是没有区别的。例如在类WordProcessor中,包含一个GetSelectedElement()静态方法,它能够获得当前选择的对象:
public class WordProcessor
{
public static IElement GetSelectedElement(){……}
}
对于字处理软件的UI来说,如果要改变选中对象的字体,则可以在命令按钮cmdChangeFont的Click事件中写下如下代码:
public void cmdChangeFont_Click(object sender, EventArgs e)
{
WordProcessor.GetSelectedElement().ChangeFont(currentFont);
}
不管当前选中的对象是文字、段落还是整篇文档,对于UI而言,操作都是完全一致的,根本不需要去判断对象的类别。因此,如果在Business Layer的类库设计时,采用Composite模式,将极大地简化UI表示层的开发工作。此外,应用该模式也较好的支持项目的可扩展性。例如,我们为IElement接口增加了Sentence类,对于前面的例子而言,只需要修改GetSelectedElement()方法,而cmdChangeFont命令按钮的Click事件以及Business Layer类库原有的设计,都不需要做任何改变。这也符合OO的开放-封闭原则(OCP),即对于扩展是开放的(Open for extension),对于更改则是封闭的(Closed for modification)。
二、.Net Framework中的Composite模式
在.Net中,最能体现Composite模式的莫过于Windows或Web的控件。在这些控件中,有的包含子控件,有的则不包含且不能包含子控件,这正好符合叶节点和枝节点的含义。所有Web控件的基类为System.Web.UI.Contril类(如果是Windows控件,则基类为System.Windows.Forms.Control类)。其子类包含有HtmlControl、HtmlContainerControl等。按照Composite模式的结构,枝节点和叶节点属于根节点的不同分支,同时枝节点与根节点之间应具备一个聚合关系,可以通过Add()、Remove()方法添加和移除其子节点。设定HtmlControl为叶节点,而HtmlContaiinerControl为枝节点,那么采用透明方式的设计方法,在.Net中控件类的结构,就应该如下图所示:
虽然根据透明方式的Composite模式,HtmlControl类与其父类Control之间也应具备一个聚合关系,但实质上该类并不具备管理子控件的职责,因此我在类图中忽略了这个关系。此时,HtmlControl类中的Add()、Remove()方法,应该为空,或者抛出一个客户端能够捕获的异常。
然而,从具体实现来考虑,由于HtmlControl类和HtmlContainerControl类在实现细节层次,区别仅在于前者不支持子控件,但从控件本身的功能来看,很多行为是相同或者相近的。例如HtmlControl类的Render()方法,调用了方法RenderBeginTag()方法:
protected override void Render(HtmlTextWriter writer)
{
this.RenderBeginTag(writer);
}
protected virtual void RenderBeginTag(HtmlTextWriter writer)
{
writer.WriteBeginTag(this.TagName);
this.RenderAttributes(writer);
writer.Write('>');
}
而HtmlContainerControl类也具有Render()方法,在这个方法中也调用了RenderBeginTag()方法,且RenderBeginTag方法的实现和前者完全一致:
protected override void Render(HtmlTextWriter writer)
{
this.RenderBeginTag(writer);
this.RenderChildren(writer);
this.RenderEndTag(writer);
}
按照上面的结构,由于HtmlControl和HtmlContainerControl之间并无继承关系,这就要求两个类中,都要重复实现RenderBeginTag()方法,从而导致产生重复代码。根据OO的特点,解决的办法,就是让HtmlContainerControl继承自HtmlControl类(因为HtmlContainerControl的接口比HtmlControl宽,所以只能令HtmlContainerControl作为子类),并让RenderBeginTag()方法成为HtmlControl类的protected方法,子类HtmlContainerControl可以直接调用这个方法。然而与之矛盾的是,HtmlContainerControl却是一个可以包含子控件的枝节点,而HtmlControl则是不能包含子控件的叶节点,那么这样的继承关系还成立吗?
HtmlControl类对Add()方法和Remove()方法的重写后,这两个方法内容为空。由于HtmlContainerControl类继承HtmlControl类,但我们又要求它的Add()和Remove()方法和Control类保持一致,而父类HtmlControl已经重写这两个方法,此时是无法直接继承来自父类的方法的。以上是采用透明方式的设计。
如果采用安全方式,仍然有问题。虽然在HtmlControl类中不再有Add()和Remove()方法,但由于Control类和HtmlContainerControl类都允许添加子控件,它们包含的Add()、Remove()方法,只能分别实现。这样的设计必然会导致重复代码。这也是与我们的期望不符的。
那么在.Net中,Control类究竟是怎样实现的呢?下面,我将根据.Net实现Control控件的源代码,来分析Control控件的真实结构,以及其具体的实现细节。
三、深入分析.Net中的Composite模式
首先,我们来剖析Web控件的基类Control类的内部实现:
public class Control : IComponent, IDisposable, IParserAccessor, IDataBindingsAccessor
{
// Events;略
// Methods
public Control()
{
if (this is INamingContainer)
{
this.flags[0×80] = true;
}
}
public virtual bool HasControls()
{
if (this._controls != null)
{
return (this._controls.Count > 0);
}
return false;
}
public virtual void DataBind()
{
this.OnDataBinding(EventArgs.Empty);
if (this._controls != null)
{
string text1 = this._controls.SetCollectionReadOnly("Parent_collections_readonly");
int num1 = this._controls.Count;
for (int num2 = 0; num2 < num1; num2++)
{
this._controls[num2].DataBind();
}
this._controls.SetCollectionReadOnly(text1);
}
}
protected virtual void Render(HtmlTextWriter writer)
{
this.RenderChildren(writer);
}
protected virtual ControlCollection CreateControlCollection()
{
return new ControlCollection(this);
}
// Properties
public virtual ControlCollection Controls
{
get
{
if (this._controls == null)
{
this._controls = this.CreateControlCollection();
}
return this._controls;
}
}
// Fields
private ControlCollection _controls;
}
Control基类中的属性和方法很多,为清晰起见,我只保留了几个与模式有关的关键方法与属性。在上述的源代码中,我们需要注意几点:
1、Control类不是抽象类,而是具体类。这是因为在设计时,我们可能会创建Control类型的实例。根据这一点来看,这并不符合OOP的要求。一般而言,作为抽象出来的基类,必须定义为接口或抽象类。不过在实际的设计中,也不应拘泥于这些条条框框,而应审时度势,根据实际的情况来抉择最佳的设计方案。
2、公共属性Controls为ControlCollection类型,且该属性为virtual属性。也就是说,这个属性可以被它的子类override。同时,该属性为只读属性,在其get访问器中,调用了方法CreateControlCollection();这个方法为protected虚方法,默认的实现是返回一个ControlCollection实例。
3、方法HasControls(),功能为判断Control对象是否有子控件。它判断的依据是根据私有字段_controls(即公共属性Controls)的Count值。但是需要注意的是,通过HasControls()方法的返回值,并不能决定对象本身属于叶节点,还是枝节点。因为即使是枝节点其内部仍然可以不包含任何子对象。
4、 方法DataBind()的实现中,首先调用了自身的OnDataBinding()方法,然后又遍历了Controls中的所有控件,并调用其DataBind()方法。该方法属于控件的共有行为,从这里可以看出不管是作为叶节点的控件,还是作为枝节点的控件,它们都实现统一的接口。对于客户端调用而言,枝节点和叶节点是没有区别的。
5、 Control类的完整源代码中,并不存在Add()、Remove()等类似的方法,以提供添加和移除子控件的功能。事实上,继承Control类的所有子类均不存在Add()、Remove()等方法。
显然,在Control类的定义和实现中,值得我们重视的是公共属性Controls的类型ControlCollection。顾名思义,该类必然是一个集合类型。是否有关子控件的操作,都是在ControlCollection类型中实现呢?我们来分析一下ControlCollection的代码:
public class ControlCollection : ICollection, IEnumerable
{
// Methods
public ControlCollection(Control owner)
{
this._readOnlyErrorMsg = null;
if (owner == null)
{
throw new ArgumentNullException("owner");
}
this._owner = owner;
}
public virtual void Add(Control child)
{
if (child == null)
{
throw new ArgumentNullException("child");
}
if (this._readOnlyErrorMsg != null)
{
throw new HttpException(HttpRuntime.FormatResourceString(this._readOnlyErrorMsg));
}
if (this._controls == null)
{
this._controls = new Control[5];
}
else if (this._size >= this._controls.Length)
{
Control[] controlArray1 = new Control[this._controls.Length * 4];
Array.Copy(this._controls, controlArray1, this._controls.Length);
this._controls = controlArray1;
}
int num1 = this._size;
this._controls[num1] = child;
this._size++;
this._version++;
this._owner.AddedControl(child, num1);
}
public virtual void Remove(Control value)
{
int num1 = this.IndexOf(value);
if (num1 >= 0)
{
this.RemoveAt(num1);
}
}
// Indexer
public virtual Control this[int index]
{
get
{
if ((index < 0) || (index >= this._size))
{
throw new ArgumentOutOfRangeException("index");
}
return this._controls[index];
}
}
// Properties
public int Count
{
get
{
return this._size;
}
}
protected Control Owner
{
get
{
return this._owner;
}
}
protected Control Owner { get; }
// Fields
private Control[] _controls;
private const int _defaultCapacity = 5;
private const int _growthFactor = 4;
private Control _owner;
}
一目了然,正是ControlCollection的Add()、Remove()方法完成了对子控件的添加和删除。例如:
Control parent = new Control();
Control child = new Child();
//添加子控件child;
parent.Controls.Add(child);
//移除子控件child;
parent.Controls.Remove(child);
为什么要专门提供ControlCollection类型来管理控件的子控件呢?首先,作为类库使用者,自然希望各种类型的控件具有统一的接口,尤其是自定义控件的时候,不希望自己重复定义管理子控件的操作;那么采用透明方式自然是最佳方案。然而,在使用控件的时候,安全也是需要重点考虑的,如果不考虑子控件管理的合法性,一旦使用错误,会导致整个应用程序出现致命错误。从这样的角度考虑,似乎又应采用安全方式。这里就存在一个抉择。故而,.Net在实现Control类库时,利用了职责分离的原则,将控件对象管理子控件的属性与行为和控件本身分离,并交由单独的ControlCollection类负责。同时采用聚合而非继承的方式,以一个公共属性Controls,存在于Control类中。这种方式,集保留了透明方式和安全方式的优势,又摒弃了这两种方式固有的缺陷,因此我名其为"复合方式"。
"复合方式"的设计,其对安全的保障,不仅仅是去除了Control类关于子控件管理的统一接口,同时还通过异常管理的方式,在ControlCollection类的子类中实现:
public class EmptyControlCollection : ControlCollection
{
// Methods
public EmptyControlCollection(Control owner) : base(owner)
{}
public override void Add(Control child)
{
this.ThrowNotSupportedException();
}
private void ThrowNotSupportedException()
{
throw new HttpException(HttpRuntime.FormatResourceString("Control_does_not_allow_children", base.Owner.GetType().ToString()));
}
}
EmptyControlCollection继承了ControlCollection类,并重写了Add()等添加子控件的方法,使其抛出一个异常。注意,它并没有重写父类的Remove()方法,这是因为ControlCollection类在实现Remove()方法时,对集合内的数据进行了非空判断。而在EmptyControlCollection类中,是不可能添加子控件的,直接调用父类的Remove()方法,是不会出现错误的。
既然管理子控件的职责由ControlCollection类型负责,且Control类中的公共属性Controls即为ControlCollection类型。所以,对于控件而言,如果是树形结构中的叶节点,它不能包含子控件,它的Controls属性就应为EmptyControlCollection类型,假如用户调用了Controls的Add()方法,就会抛出异常。如果控件是树形结构中的枝节点,它支持子控件,那么Controls属性就是ControlCollection类型。究竟是枝节点还是叶节点,决定权在于公共属性Controls:
public virtual ControlCollection Controls
{
get
{
if (this._controls == null)
{
this._controls = this.CreateControlCollection();
}
return this._controls;
}
}
在属性的get访问器中,调用了protected方法CreateControlCollection(),它创建并返回了一个ControlCollection实例:
protected virtual ControlCollection CreateControlCollection()
{
return new ControlCollection(this);
}
很明显,在Control基类实现Controls属性时,采用了Template Method模式,它推迟了ControlCollection的创建,将决定权交给了CreateControlCollection()方法。
如果我们需要定义一个控件,要求它不能管理子控件,就重写CreateControlCollection()方法,返回EmptyControlCollection对象:
protected override ControlCollection CreateControlCollection()
{
return new EmptyControlCollection(this);
}
现在再回过头来看HtmlControl和HtmlContainerControl类。根据前面的分析,我们要求HtmlContainerControl继承HtmlControl类,同时,HtmlContainerControl应为枝节点,能够管理子控件;HtmlControl则为叶节点,不支持子控件。通过引入ControlCollection类和其子类EmptyControlCollection,以及Template Method模式后,这些类之间的关系与结构如下所示:
HtmlContainerControl继承了HtmlControl类,这两个类都重写了自己父类的protected方法CreateControlCollection()。HtmlControl类,该方法返回EmptyControlCollection对象,使其成为了不包含子控件的叶节点;HtmlContainerControl类中,该方法则返回ControlCollection对象,从而被赋予了管理子控件的能力,成为了枝节点:
public abstract class HtmlControl : Control, IAttributeAccessor
{
// Methods
protected override ControlCollection CreateControlCollection()
{
return new EmptyControlCollection(this);
}
}
public abstract class HtmlContainerControl : HtmlControl
{
// Methods
protected override ControlCollection CreateControlCollection()
{
return new ControlCollection(this);
}
}
HtmlControl和HtmlContainerControl类均为抽象类。要定义它们的子类,如果不重写其父类的CreateControlCollection()方法,那么它们的Controls属性,就与父类完全一致。例如HtmlImage控件继承自HtmlControl类,该控件不能添加子控件;而HtmlForm控件则继承自HtmlContainerControl类,显然,HtmlForm控件是支持添加子控件的操作的。
.Net的控件设计采用Composite模式的"复合方式",较好地将控件的透明性与安全性结合起来,它的特点是:
1、在统一接口中消除了Add()、Remove()等子控件的管理方法,而由ControlCollection类实现,同时通过EmptyControlCollection类保障了控件进一步的安全;
2、控件能否管理子控件,不由继承的层次决定;而是通过重写CreateControlCollection()方法,由Controls属性的真正类型来决定。
如此一来,要定义自己的控件就更加容易。我们可以任意地扩展自己的控件类。不管继承自Control,还是HtmlControl或HtmlContainerControl,都可以轻松地定义出具有枝节点或叶节点属性的新控件。如果有新的需求要求改变管理子控件的方式,我们还可以定义继承自ControlCollection的类,并在控件类的方法CreateControlCollection()中创建并返回它的实例。
组合模式(Composite Pattern)
——.NET设计模式系列之十一
Terrylee,2006年3月
概述
组合模式有时候又叫做部分-整体模式,它使我们树型结构的问题中,模糊了简单元素和复杂元素的概念,客户程序可以向处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素的内部结构解耦。
意图
将对象组合成树形结构以表示"部分-整体"的层次结构。Composite模式使得用户对单个对象和组合对象的使用具有一致性。[GOF 《设计模式》]
结构图
图1 Composite模式结构图
生活中的例子
组合模式将对象组合成树形结构以表示"部分-整体"的层次结构。让用户一致地使用单个对象和组合对象。虽然例子抽象一些,但是算术表达式确实是组合的例子。算术表达式包括操作数、操作符和另一个操作数。操作数可以是数字,也可以是另一个表达式。这样,2+3和(2+3)+(4*6)都是合法的表达式。
图2 使用算术表达式例子的Composite模式对象图
组合模式解说
这里我们用绘图这个例子来说明Composite模式,通过一些基本图像元素(直线、圆等)以及一些复合图像元素(由基本图像元素组合而成)构建复杂的图形树。在设计中我们对每一个对象都配备一个Draw()方法,在调用时,会显示相关的图形。可以看到,这里复合图像元素它在充当对象的同时,又是那些基本图像元素的一个容器。先看一下基本的类结构图:
图3
图中橙色的区域表示的是复合图像元素。示意性代码:
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()等方法,类结构图如下:
图4
示意性代码:
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使用这些方法时,在编译期就会出错,看一下类结构图:
图5
示意性代码:
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模式的例子,来看它的结构图:
图6
正如我们在图中所看到的,IReadableLocator定义了最上层的定位器接口方法,它基本上具备了定位器的大部分功能。
部分代码:
public interface IReadableLocator : IEnumerable < object, object>>
{
//返回定位器中节点的数量
int Count { get; }
//一个指向父节点的引用
IReadableLocator ParentLocator { get; }
//表示定位器是否只读
bool ReadOnly { get; }
//查询定位器中是否已经存在指定键值的对象
bool Contains(object key);
//查询定位器中是否已经存在指定键值的对象,根据给出的搜索选项,表示是否要向上回溯继续寻找。
bool Contains(object key, SearchMode options);
//使用谓词操作来查找包含给定对象的定位器
IReadableLocator FindBy(Predicate < object, object>> predicate);
//根据是否回溯的选项,使用谓词操作来查找包含对象的定位器
IReadableLocator FindBy(SearchMode options, Predicate < object, object>> predicate);
//从定位器中获取一个指定类型的对象
TItem Get ();
//从定位其中获取一个指定键值的对象
TItem Get ( object key);
//根据选项条件,从定位其中获取一个指定类型的对象
TItem Get ( object key, SearchMode options);
//给定对象键值获取对象的非泛型重载方法
object Get(object key);
//给定对象键值带搜索条件的非泛型重载方法
object Get(object key, SearchMode options);
}
一个抽象基类ReadableLocator用来实现这个接口的公共方法。两个主要的方法实现代码如下:
public abstract class ReadableLocator : IReadableLocator
{
///
/// 查找定位器,最后返回一个只读定位器的实例
///
public IReadableLocator FindBy(SearchMode options, Predicate < 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);
}
///
/// 遍历定位器
///
private void FindInLocator(Predicate < 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,在这个接口里面提供了实现定位器的操作:
图7
实现代码如下:
public interface IReadWriteLocator : IReadableLocator
{
//保存对象到定位器
void Add(object key, object value);
//从定位器中删除一个对象,如果成功返回真,否则返回假
bool Remove(object key);
}
从ReadWirteLocator派生的具体类是Locator类,Locator类必须实现一个定位器的全部功能,现在我们所看到的Locator它已经具有了管理定位器的功能,同时他还应该具有存储的结构,这个结构是通过一个WeakRefDictionary类来实现的,这里就不介绍了。[关于定位器的介绍参考了niwalker的Blog]
效果及实现要点
1.Composite模式采用树形结构来实现普遍存在的对象容器,从而将"一对多"的关系转化"一对一"的关系,使得客户代码可以一致地处理对象和对象容器,无需关心处理的是单个的对象,还是组合的对象容器。
2.将"客户代码与复杂的对象容器结构"解耦是Composite模式的核心思想,解耦之后,客户代码将与纯粹的抽象接口——而非对象容器的复内部实现结构——发生依赖关系,从而更能"应对变化"。
3.Composite模式中,是将"Add和Remove等和对象容器相关的方法"定义在"表示抽象对象的Component类"中,还是将其定义在"表示对象容器的Composite类"中,是一个关乎"透明性"和"安全性"的两难问题,需要仔细权衡。这里有可能违背面向对象的"单一职责原则",但是对于这种特殊结构,这又是必须付出的代价。ASP.NET控件的实现在这方面为我们提供了一个很好的示范。
4.Composite模式在具体实现中,可以让父对象中的子对象反向追溯;如果父对象有频繁的遍历需求,可使用缓存技巧来改善效率。
适用性
以下情况下适用Composite模式:
1.你想表示对象的部分-整体层次结构
2.你希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。
总结
组合模式解耦了客户程序与复杂元素内部结构,从而使客户程序可以向处理简单元素一样来处理复杂元素。
Pasted from <http://terrylee.cnblogs.com/archive/2006/03/11/347919.html>