`

第10章 组合模式(Composite Pattern)

 
阅读更多

组合模式(Composite Pattern)

——.NET设计模式系列之十一

Terrylee,2006年3月

概述

组合模式有时候又叫做部分-整体模式,它使我们树型结构的问题中,模糊了简单元素和复杂元素的概念,客户程序可以向处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素的内部结构解耦。

意图

将对象组合成树形结构以表示“部分-整体”的层次结构。Composite模式使得用户对单个对象和组合对象的使用具有一致性。[GOF 《设计模式》]

结构图

图1 Composite模式结构图

生活中的例子

组合模式将对象组合成树形结构以表示"部分-整体"的层次结构。让用户一致地使用单个对象和组合对象。虽然例子抽象一些,但是算术表达式确实是组合的例子。算术表达式包括操作数、操作符和另一个操作数。操作数可以是数字,也可以是另一个表达式。这样,2+3和(2+3)+(4*6)都是合法的表达式。

图2 使用算术表达式例子的Composite模式对象图

组合模式解说

这里我们用绘图这个例子来说明Composite模式,通过一些基本图像元素(直线、圆等)以及一些复合图像元素(由基本图像元素组合而成)构建复杂的图形树。在设计中我们对每一个对象都配备一个Draw()方法,在调用时,会显示相关的图形。可以看到,这里复合图像元素它在充当对象的同时,又是那些基本图像元素的一个容器。先看一下基本的类结构图:

图3

图中橙色的区域表示的是复合图像元素。示意性代码:

publicabstractclassGraphics
{
protectedstring_name;

publicGraphics(stringname)
{
this._name=name;
}
publicabstractvoidDraw();
}

publicclassPicture:Graphics
{
publicPicture(stringname)
:base(name)
{}
publicoverridevoidDraw()
{
//
}

publicArrayListGetChilds()
{
//返回所有的子对象
}
}

而其他作为树枝构件,实现代码如下:

publicclassLine:Graphics
{
publicLine(stringname)
:base(name)
{}

publicoverridevoidDraw()
{
Console.WriteLine("Drawa"+_name.ToString());
}
}

publicclassCircle:Graphics
{
publicCircle(stringname)
:base(name)
{}

publicoverridevoidDraw()
{
Console.WriteLine("Drawa"+_name.ToString());
}
}

publicclassRectangle:Graphics
{
publicRectangle(stringname)
:base(name)
{}

publicoverridevoidDraw()
{
Console.WriteLine("Drawa"+_name.ToString());
}
}

现在我们要对该图像元素进行处理:在客户端程序中,需要判断返回对象的具体类型到底是基本图像元素,还是复合图像元素。如果是复合图像元素,我们将要用递归去处理,然而这种处理的结果却增加了客户端程序与复杂图像元素内部结构之间的依赖,那么我们如何去解耦这种关系呢?我们希望的是客户程序可以像处理基本图像元素一样来处理复合图像元素,这就要引入Composite模式了,需要把对于子对象的管理工作交给复合图像元素,为了进行子对象的管理,它必须提供必要的Add(),Remove()等方法,类结构图如下:

图4

示意性代码:

publicabstractclassGraphics
{
protectedstring_name;

publicGraphics(stringname)
{
this._name=name;
}
publicabstractvoidDraw();
publicabstractvoidAdd();
publicabstractvoidRemove();
}

publicclassPicture:Graphics
{
protectedArrayListpicList=newArrayList();

publicPicture(stringname)
:base(name)
{}
publicoverridevoidDraw()
{
Console.WriteLine("Drawa"+_name.ToString());

foreach(GraphicsginpicList)
{
g.Draw();
}
}

publicoverridevoidAdd(Graphicsg)
{
picList.Add(g);
}
publicoverridevoidRemove(Graphicsg)
{
picList.Remove(g);
}
}

publicclassLine:Graphics
{
publicLine(stringname)
:base(name)
{}

publicoverridevoidDraw()
{
Console.WriteLine("Drawa"+_name.ToString());
}
publicoverridevoidAdd(Graphicsg)
{}
publicoverridevoidRemove(Graphicsg)
{}
}

publicclassCircle:Graphics
{
publicCircle(stringname)
:base(name)
{}

publicoverridevoidDraw()
{
Console.WriteLine("Drawa"+_name.ToString());
}
publicoverridevoidAdd(Graphicsg)
{}
publicoverridevoidRemove(Graphicsg)
{}
}

publicclassRectangle:Graphics
{
publicRectangle(stringname)
:base(name)
{}

publicoverridevoidDraw()
{
Console.WriteLine("Drawa"+_name.ToString());
}
publicoverridevoidAdd(Graphicsg)
{}
publicoverridevoidRemove(Graphicsg)
{}
}

这样引入Composite模式后,客户端程序不再依赖于复合图像元素的内部实现了。然而,我们程序中仍然存在着问题,因为Line,Rectangle,Circle已经没有了子对象,它是一个基本图像元素,因此Add(),Remove()的方法对于它来说没有任何意义,而且把这种错误不会在编译的时候报错,把错误放在了运行期,我们希望能够捕获到这类错误,并加以处理,稍微改进一下我们的程序:

publicclassLine:Graphics
{
publicLine(stringname)
:base(name)
{}

publicoverridevoidDraw()
{
Console.WriteLine("Drawa"+_name.ToString());
}
publicoverridevoidAdd(Graphicsg)
{
//抛出一个我们自定义的异常
}
publicoverridevoidRemove(Graphicsg)
{
//抛出一个我们自定义的异常
}
}

这样改进以后,我们可以捕获可能出现的错误,做进一步的处理。上面的这种实现方法属于透明式的Composite模式,如果我们想要更安全的一种做法,就需要把管理子对象的方法声明在树枝构件Picture类里面,这样如果叶子节点Line,Rectangle,Circle使用这些方法时,在编译期就会出错,看一下类结构图:

图5

示意性代码:

publicabstractclassGraphics
{
protectedstring_name;

publicGraphics(stringname)
{
this._name=name;
}
publicabstractvoidDraw();
}

publicclassPicture:Graphics
{
protectedArrayListpicList=newArrayList();

publicPicture(stringname)
:base(name)
{}
publicoverridevoidDraw()
{
Console.WriteLine("Drawa"+_name.ToString());

foreach(GraphicsginpicList)
{
g.Draw();
}
}

publicvoidAdd(Graphicsg)
{
picList.Add(g);
}
publicvoidRemove(Graphicsg)
{
picList.Remove(g);
}
}

publicclassLine:Graphics
{
publicLine(stringname)
:base(name)
{}

publicoverridevoidDraw()
{
Console.WriteLine("Drawa"+_name.ToString());
}
}

publicclassCircle:Graphics
{
publicCircle(stringname)
:base(name)
{}

publicoverridevoidDraw()
{
Console.WriteLine("Drawa"+_name.ToString());
}
}

publicclassRectangle:Graphics
{
publicRectangle(stringname)
:base(name)
{}

publicoverridevoidDraw()
{
Console.WriteLine("Drawa"+_name.ToString());
}
}

这种方式属于安全式的Composite模式,在这种方式下,虽然避免了前面所讨论的错误,但是它也使得叶子节点和树枝构件具有不一样的接口。这种方式和透明式的Composite各有优劣,具体使用哪一个,需要根据问题的实际情况而定。通过Composite模式,客户程序在调用Draw()的时候不用再去判断复杂图像元素中的子对象到底是基本图像元素,还是复杂图像元素,看一下简单的客户端调用:

publicclassApp
{
publicstaticvoidMain()
{
Pictureroot=newPicture("Root");

root.Add(newLine("Line"));
root.Add(newCircle("Circle"));

Rectangler=newRectangle("Rectangle");
root.Add(r);

root.Draw();
}
}

.NET中的组合模式

如果有人用过Enterprise Library2.0,一定在源程序中看到了一个叫做ObjectBuilder的程序集,顾名思义,它是用来负责对象的创建工作的,而在ObjectBuilder中,有一个被称为定位器的东西,通过定位器,可以很容易的找到对象,它的结构采用链表结构,每一个节点是一个键值对,用来标识对象的唯一性,使得对象不会被重复创建。定位器的链表结构采用可枚举的接口类来实现,这样我们可以通过一个迭代器来遍历这个链表。同时多个定位器也被串成一个链表。具体地说就是多个定位器组成一个链表,表中的每一个节点是一个定位器,定位器本身又是一个链表,表中保存着多个由键值对组成的对象的节点。所以这是一个典型的Composite模式的例子,来看它的结构图:


图6

正如我们在图中所看到的,IReadableLocator定义了最上层的定位器接口方法,它基本上具备了定位器的大部分功能。

部分代码:

publicinterfaceIReadableLocator:IEnumerable<KeyValuePair<object,object>>
{
//返回定位器中节点的数量
intCount{get;}

//一个指向父节点的引用
IReadableLocatorParentLocator{get;}

//表示定位器是否只读
boolReadOnly{get;}

//查询定位器中是否已经存在指定键值的对象
boolContains(objectkey);

//查询定位器中是否已经存在指定键值的对象,根据给出的搜索选项,表示是否要向上回溯继续寻找。
boolContains(objectkey,SearchModeoptions);

//使用谓词操作来查找包含给定对象的定位器
IReadableLocatorFindBy(Predicate<KeyValuePair<object,object>>predicate);

//根据是否回溯的选项,使用谓词操作来查找包含对象的定位器
IReadableLocatorFindBy(SearchModeoptions,Predicate<KeyValuePair<object,object>>predicate);

//从定位器中获取一个指定类型的对象
TItemGet<TItem>();

//从定位其中获取一个指定键值的对象
TItemGet<TItem>(objectkey);

//根据选项条件,从定位其中获取一个指定类型的对象
TItemGet<TItem>(objectkey,SearchModeoptions);

//给定对象键值获取对象的非泛型重载方法
objectGet(objectkey);

//给定对象键值带搜索条件的非泛型重载方法
objectGet(objectkey,SearchModeoptions);
}

一个抽象基类ReadableLocator用来实现这个接口的公共方法。两个主要的方法实现代码如下:

publicabstractclassReadableLocator:IReadableLocator
{
/**////<summary>
///查找定位器,最后返回一个只读定位器的实例
///</summary>
publicIReadableLocatorFindBy(SearchModeoptions,Predicate<KeyValuePair<object,object>>predicate)
{
if(predicate==null)
thrownewArgumentNullException("predicate");
if(!Enum.IsDefined(typeof(SearchMode),options))
thrownewArgumentException(Properties.Resources.InvalidEnumerationValue,"options");

Locatorresults=newLocator();
IReadableLocatorcurrentLocator=this;

while(currentLocator!=null)
{
FindInLocator(predicate,results,currentLocator);
currentLocator=options==SearchMode.Local?null:currentLocator.ParentLocator;
}

returnnewReadOnlyLocator(results);
}

/**////<summary>
///遍历定位器
///</summary>
privatevoidFindInLocator(Predicate<KeyValuePair<object,object>>predicate,Locatorresults,
IReadableLocatorcurrentLocator)
{
foreach(KeyValuePair<object,object>kvpincurrentLocator)
{
if(!results.Contains(kvp.Key)&&predicate(kvp))
{
results.Add(kvp.Key,kvp.Value);
}
}
}
}

可以看到,在FindBy方法里面,循环调用了FindInLocator方法,如果查询选项是只查找当前定位器,那么循环终止,否则沿着定位器的父定位器继续向上查找。FindInLocator方法就是遍历定位器,然后把找到的对象存入一个临时的定位器。最后返回一个只读定位器的新的实例。

从这个抽象基类中派生出一个具体类和一个抽象类,一个具体类是只读定位器(ReadOnlyLocator),只读定位器实现抽象基类没有实现的方法,它封装了一个实现了IReadableLocator接口的定位器,然后屏蔽内部定位器的写入接口方法。另一个继承的是读写定位器抽象类ReadWriteLocator,为了实现对定位器的写入和删除,这里定义了一个对IReadableLocator接口扩展的接口叫做IReadWriteLocator,在这个接口里面提供了实现定位器的操作:


图7

实现代码如下:

publicinterfaceIReadWriteLocator:IReadableLocator
{
//保存对象到定位器
voidAdd(objectkey,objectvalue);

//从定位器中删除一个对象,如果成功返回真,否则返回假
boolRemove(objectkey);
}

从ReadWirteLocator派生的具体类是Locator类,Locator类必须实现一个定位器的全部功能,现在我们所看到的Locator它已经具有了管理定位器的功能,同时他还应该具有存储的结构,这个结构是通过一个WeakRefDictionary类来实现的,这里就不介绍了。[关于定位器的介绍参考了niwalker的Blog]

效果及实现要点

1.Composite模式采用树形结构来实现普遍存在的对象容器,从而将“一对多”的关系转化“一对一”的关系,使得客户代码可以一致地处理对象和对象容器,无需关心处理的是单个的对象,还是组合的对象容器。

2.将“客户代码与复杂的对象容器结构”解耦是Composite模式的核心思想,解耦之后,客户代码将与纯粹的抽象接口——而非对象容器的复内部实现结构——发生依赖关系,从而更能“应对变化”。

3.Composite模式中,是将“Add和Remove等和对象容器相关的方法”定义在“表示抽象对象的Component类”中,还是将其定义在“表示对象容器的Composite类”中,是一个关乎“透明性”和“安全性”的两难问题,需要仔细权衡。这里有可能违背面向对象的“单一职责原则”,但是对于这种特殊结构,这又是必须付出的代价。ASP.NET控件的实现在这方面为我们提供了一个很好的示范。

4.Composite模式在具体实现中,可以让父对象中的子对象反向追溯;如果父对象有频繁的遍历需求,可使用缓存技巧来改善效率。

适用性

以下情况下适用Composite模式:

1.你想表示对象的部分-整体层次结构

2.你希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。

总结

组合模式解耦了客户程序与复杂元素内部结构,从而使客户程序可以向处理简单元素一样来处理复杂元素。

参考资料

阎宏,《Java与模式》,电子工业出版社

James W. Cooper,《C#设计模式》,电子工业出版社

Alan Shalloway James R. Trott,《Design Patterns Explained》,中国电力出版社

MSDN WebCast 《C#面向对象设计模式纵横谈(9):Composite组合模式(结构型模式)》

作者:TerryLee
出处:http://terrylee.cnblogs.com
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
分享到:
评论

相关推荐

    24个设计模式与6大设计原则

    第 15 章 组合模式【COMPOSITE PATTERN】 147 第 16 章 观察者模式【OBSERVER PATTERN】 175 第 17 章 责任链模式【CHAIN OF RESPONSIBILITY PATTERN】 194 第 18 章 访问者模式【VISITOR PATTERN...

    设计模式 design pattern

    2.2.3 组合模式 27 2.3 格式化 27 2.3.1 封装格式化算法 27 2.3.2 Compositor和Composition 27 2.3.3 策略模式 29 2.4 修饰用户界面 29 2.4.1 透明围栏 29 2.4.2 Monoglyph 30 2.4.3 Decorator 模式 32 2.5 支持多种...

    [源代码] 《易学 设计模式》 随书源代码

    第14章 如法炮制:组合模式 (Composite) 第15章 源源不断:享元模式 (Flyweight) 第16章 按部就班:模板方法模式 (TemplateMethod) 第17章 风吹草动:观察者模式 (Observer) 第18章 变化多端:状态模式 (State) 第19...

    Java设计模式

    第 15 章 组合模式【COMPOSITE PATTERN】 ............................................................................................ 147 第 16 章 观察者模式【OBSERVER PATTERN】 ...........................

    《设计模式》中文版(23个设计模式的介绍与运用)

    2.2.3 组合模式 27 2.3 格式化 27 2.3.1 封装格式化算法 27 2.3.2 Compositor和Composition 27 2.3.3 策略模式 29 2.4 修饰用户界面 29 2.4.1 透明围栏 29 2.4.2 Monoglyph 30 2.4.3 Decorator 模式 32 2.5 支持多种...

    设计模式 GOF 23

    2.2.3 组合模式 27 2.3 格式化 27 2.3.1 封装格式化算法 27 2.3.2 Compositor和Composition 27 2.3.3 策略模式 29 2.4 修饰用户界面 29 2.4.1 透明围栏 29 2.4.2 Monoglyph 30 2.4.3 Decorator 模式 32 2.5 支持多种...

    设计模式:可复用面向对象软件的基础--详细书签版

    2.2.3 组合模式 27 2.3 格式化 27 2.3.1 封装格式化算法 27 2.3.2 compositor和composition 27 2.3.3 策略模式 29 2.4 修饰用户界面 29 2.4.1 透明围栏 29 2.4.2 monoglyph 30 2.4.3 decorator 模式 32 2.5...

    二十三种设计模式【PDF版】

    设计模式之 Composite(组合) 就是将类用树形结构组合成一个单位.你向别人介绍你是某单位,你是单位中的一个元素,别人和你做买卖,相当于 和单位做买卖。文章中还对 Jive再进行了剖析。 设计模式之 Decorator(装饰...

    asp.net知识库

    深入剖析ASP.NET组件设计]一书第三章关于ASP.NET运行原理讲述的补白 asp.net 运行机制初探(httpModule加载) 利用反射来查看对象中的私有变量 关于反射中创建类型实例的两种方法 ASP.Net应用程序的多进程模型 NET委托...

    java 面试题 总结

    10、&和&&的区别。 &是位运算符,表示按位与运算,&&是逻辑运算符,表示逻辑与(and)。 11、HashMap和Hashtable的区别。 HashMap是Hashtable的轻量级实现(非线程安全的实现),他们都完成了Map接口,主要区别...

    超级有影响力霸气的Java面试题大全文档

    当客户机第一次调用一个Stateful Session Bean 时,容器必须立即在服务器中创建一个新的Bean实例,并关联到客户机上,以后此客户机调用Stateful Session Bean 的方法时容器会把调用分派到与此客户机相关联的Bean实例...

Global site tag (gtag.js) - Google Analytics