1.意图
将对象组合成树形结构以表示“部分-整体”的层次结构。Composite使得用户对单个对象和组合对象的使用具有一致性。
2.动机
在绘图编辑器和图形捕捉系统这样的图形应用程序中,用户可以使用简单的组件创建复杂的图表。用户可以组合多个简单组件以形成一些较大的组件,这些组件又可以组合成更大的组件。一个简单的实现方法是为Text和Line这样的图元定义一些类,另外一些类作为这些图元的容器类(Container)。然而这种方法存在一个问题:使用这些类的代码必须区别对待图元对象与容器对象,而实际上大多数情况下用户认为它们是一样的。对这些类区别使用,使得程序更加复杂。Composite模式描述了如何使用递归组合,使得用户不必对这些类进行区别,如下图所示。
Composite模式的一个关键是一个抽象类,它既可以代表图元,又可以代表图元的容器。在图形系统中的这个类就是Graphic,它声明一些与特定图形对象相关的操作,例如Draw。同时它也声明了所有的组合对象共享的一些操作,例如一些操作用于访问和管理它的子部件。
子类Line、Rectange和Text(参加前面的类)定义了一些图元对象,这些类实现Draw,分别用于绘制直线,矩形和正文。由于图元都没有子图形,因此它们都不执行与子类有关的
操作。
Picture类定义了一个Graphic对象的聚合。Picture的Draw操作是通过对它的子部件调用Draw实现的,Picture还用这种方法实现了一些与其子部件相关的操作。由于Picture接口与Graphic接口是一致的,因此Picture对象可以递归地组合其他Picture对象。
下图是一个典型的由递归组合的Graphic对象组成的组合对象结构。
3. 适用性
4.结构
典型的Composite对象结构如下图所示。
5. 参与者
--为组合中的对象声明接口
--在适当的情况下,实现所有类共有接口的缺省行为
--声明一个接口用于访问和管理Component的子组件
--(可选)在递归结构中定义一个接口,用于访问一个父部件,并在合适的情况下实现它。
--在组合中表示叶结点,叶结点没有了子节点
--在组合中定义图元对象的行为。
--定义有子部件的那些部件的行为
--存储子部件
--在Component接口中实现与子部件有关的操作。
--通过Component接口操纵组合部件的对象。
6. 协作
用户使用Component类接口与组合结构中的对象进行交互。如果接收者是一个叶子结点,则直接处理请求。如果接收者是Composite,它通常将请求发送给它的子部件,在转发请求之前或之后可能执行一些辅助操作。
7.效果
Composite模式
基本对象可以被组合成更复杂的组合对象,而这个组合对象又可以被组合,这样不断的递归下去。客户代码中,任何用的基本对象的地方都可以使用组合对象。
客户可以一致地使用组合结构和单个对象。通常用户不知道(也不关心)处理地是一个叶节点还是一个组合节点。这就简化了客户代码,不需要写一些充斥着选择语句的函数。
新定义的Composite或Leaf类会自动地与已知的结构和客户代码一起工作,客户程序不需要因新的Component类而改变。
容易增加新组件也会产生一些问题。那就是很难限制组合中的组件。有时希望一个组合只能有某些特定的组件。使用Composite时,你不能依赖类型系统施加这些约束,而必须在运行时刻进行检查。
8. 实现
我们在实现Composite模式时需要考虑以下几个问题:
保持从子部件到父部件的引用能简化组合结构的遍历和管理。父部件引用可以简化结构的上移和组件的删除,同时父部件引用也支持ChainOfResponsibility模式。
通常在Component类中定义父部件引用。Leaf和Compositte类可以继承这个引用以及管理这个引用的那些操作。
对于父部件引用,必须维护一个不变式,即一个组合的所有子节点以这个组合为父节点,而反之该组合以这些节点为子节点。保证这一点最容易的一个方法是,仅当在一个组合中增加或实现这种方法,那么所有的子类都可以继承这个方法,并且将自动维护这一不变式。
共享组件是很有用的,可以减少对存储的需求
Composite模式的目的之一是使得用户不知道他们正在使用的具体的Leaf和Composite类。为了达到这个目的,Composite类应为Leaf和Composite类尽可能多定义一些公共操作。Composite类通常为这些操作提供缺省实现,而Leaf和Composite子类可以对他们进行重定义。
然而这个目标有时可能会与类层次结构设计原子相冲突,该原则规定:一个类只能定义那些对它的子类有意义的操作。有许多Component所支持的操作对Leaf类似乎没有什么意义,那么Component怎样为它们提供一个缺省的操作?
一个看起来对Composite才有意义的操作,将它移入Component类中,就会对所有的Component都适用。例如,访问子节点的接口是Compositor类的一个基本组成部分,但对Leaf节点类并不必要。但是如果把Leaf看成一个没有子节点的Component,就可以为在Component类中定义一个缺省的操作,用于对子节点进行访问,这个缺省的操作不返回任何一个子节点。Leaf类可以使用缺省的实现,而Composite类则会重新实现这个操作以返回它们的子类。
虽然Composite类实现了Add和Remove操作用于管理子部件,但在Composite模式中一个重要的问题是:在Composite类层次结构中哪一些类中声明这些操作。我们是应该在Component中声明这些操作,并使这些操作对Leaf类有意义呢,还是只应该在Composite和它的子类中声明并定义这些操作呢?
这需要在安全性和透明性之间做出权衡选择。
在类层次结构的根部定义子节点管理接口的方法具有良好的透明性,因为可以一致地使用所有地组件,但是这一方法是以安全性为代价的,因为客户有可能会做一些无意义的
在这一模式中,相对于安全,我们比较强调透明性。如果你选择了安全性,有时可能会丢失类型信息,并且不得不将一个组件转换成一个组合。这样的类型转换必定不是类型安全的。
一种方法是在Component类中声明一个操作Composite* GetComposite()。Component提供了一个返回空指针的缺省操作。Composite类重新定义了这个操作并通过this指针返回它自身。
Class Composite;
Class Component {
Public:
// …
Virtual Composite* GetComposite() {return 0;}
};
Class Composite: public Component {
Public:
Void Add(Component*);
//…
Virtual Composite* GetComposite() {return this;}
};
Class Leaf: public Component {
//…
};
GetComposite允许查询一个组件看它是否是一个组合,你可以对返回的组合安全的执行Add和Remove操作。
Composite* aComposite = new Composite;
Leaf* aLeaf = new Leaf;
Component* aComponent;
Composite* test;
aComponent = aComposite;
if(test = aComponent->GetComposite()) {
test->Add(new Leaf);
};
aComponent = aLeaf;
if(test = aComponent->GetComposite()) {
test->Add(new Leaf);
}
提供透明性的唯一方法是在Component中定义缺省Add和Remove操作。这又带
来了一个新的问题:Component:ADD的实现不可避免地会有失败地可能性。可以不让Component::ADD做任何事情,但这又忽略了一个很重要地问题:企图向叶节点增加一些东西时可能会引入错误。这时Add操作会产生垃圾。你可以让Add操作删除它地参数,但可能客户并不希望这样。
如果该组件不允许有子部件,或者Remove的参数不是该组件的子节点时,通常最好使用缺省方式处理Add和Remove的失败。
另一办法是对“删除”的含义作一些改变。如果该组件有一个父部件引用,我们可以重新定义Component::Remove,在它的父组件中删除该组件。然而对应的Add操作就没有合理解释了(也就是无定义)。
你可能希望在Component类中将子节点集合定义为一个实例变量,而这个Component类中也声明了一些操作对子节点进行访问和管理。但是在基类中存放子类指针,对叶节点来说导致浪费空间,因为叶节点没有子节点。只有当该结构中子类数目相对较少时,才值得使用这种方法。
许多设计指定了Composite的子部件顺序。在前面的Graphic例子中,排序可能表示了从前至后的顺序。如果Composite表示语法分析树,Composite子部件的顺序必须反映程序结构,而组合语句就是这样一些Composite的实例。
如果需要考虑子节点的顺序时,必须仔细设计对子节点的访问和管理接口,以便管理子节点序列。Iterator模式可以对这方面给予一些的指导。
如果你需要对组合进行频繁的遍历或查找,Composite类可以缓冲存储对它的子节点进行遍历或查找的相关信息。Composite可以缓冲存储实际结果或者仅仅是一些用于缩短遍历或查询长度的信息。例如,在动机一节的例子中Picture类能高速缓冲存储其子部件的边界框,在绘图或选择期间,当子部件在当前窗口不可见时,这个边界框使得Picture不需要再进行绘图或选择。
一个组件发生变化时,它的父部件原先缓冲的信息也变得无效。在组件知道其父部件时,这种方法最为有效。因此如果使用高速缓存,需要定义一个接口来通知组合组件它们所缓存的信息无效。
在没有垃圾回收机制的语言中,当一个Composite被销毁时,通常最好由Composite负责删除其子节点。但有一种情况除外,即Leaf对象不会改变,因此可以被共享。
Composite可使用多种数据结构存储它们的子结点,包括连接列表、树、数组和Hash表。数据结构的选择取决于效率。事实上,使用通用数据结构根本没有必要。有时对每个子节点,Composite都有一个变量与之对应,这就要求Composite的每个子类都要实现自己的管理接口。参加Interpreter模式的例子。
9 代码示例
计算机和立体声组合音响这样的设备经常被组装成部分-整体层次结构或者是容器层次结构。例如,底盘可包含驱动装置和平面板,总线含有多个插件,机柜包括底盘、总线等。这种结构可以很自然地用Composite模式进行模拟。
Equipment类为在部分-整体层次结构中的所有设备定义了一个接口。
Class Equipment {
Public:
Virtual ~Equipment();
Const char* Name() {return _name;}
Virtual Watt Power();
Virtual Currency NetPrice();
Virtual Currency DiscountPrice();
Virtual void Add(Equipment*);
Virtual void Remove(Equipment*);
Virtual void Iterator
Protected:
Equipment(const char*);
Private:
Const char* _name;
};
Equipment声明一些操作返回一个设备的属性,例如它的能量消耗和价格。子类为指定的设备实现这些操作,Equipment还声明了一个CreateIterator操作,该操作作为访问它的零件返回一个Iterator。这个操作的缺省实现返回一个NullIterator,它在空集上迭代。
Equipment的子类包括表示磁盘驱动器、集成电路和开关的Leaf类。
Class FloppyDisk: public Equipment {
Public:
FloppyDisk(const char*);
Virtual ~FloppyDisk();
Virtual Watt Power();
Virtual Currency NetPrice();
Virtual Currency DiscountPrice();
};
CompositeEquipment是包含其他设备的基类,它也是Equipment的子类。
Class CompositeEquipment : public Equipment {
Public:
Virtual ~CompositeEquipment();
Virtual Watt Power();
Virtual Currency NetPrice();
Virtual Currency DiscountPrice();
Virtual void Add(Equipment*);
Virtual void Remove(Equipment*);
Virtual Iterator
Protected:
CompositeEquipment(const char*);
Private:
List
};
CompositeEquipment为访问和管理子设备定义了一些操作。操作Add和Remove从存储在_equipment成员变量中的设备列表中插入并删除设备。操作CreateIterator返回一个迭代器(ListIterator的一个实例)遍历整个列表。
NetPrice的缺省实现使用CreateIterator来累加子设备的实际价格。
Currency CompositeEquipment::NetPrice() {
Iterator
Currency total =0;
For(i->First(); !i->IsDone(); i->Next()) {
Total += i->CurrentItem()->NetPrice();
}
Delete I;
Return total;
}
现在我们将计算机的底盘表示为CompositeEquipment的子类Chasis。Chassis从CompositeEquipment继承了与子类相关的那些操作。
Class Chassis: public CompositeEquipment {
Public:
Chassis(const char*);
Virtual ~Chassis();
Virtual Watt Power();
Virtual Currency NetPrice();
Virtual Currency DiscountPrice();
};
我们可以用相似的方式定义其他设备容器,如Cabinet和Bus。这样我们就得到了组装一台个人计算机所需的所有设备。
Cabinet* cabinet = new Cabinet(“PC cabinnet”);
Chassis* chassis = new Chassis(“PC chassis”);
Cabinet->Add(chassis);
Bus* bus = new Bus(“MCA Bus”);
Bus ->(new Card(“16Mbs Token ring”));
Chassis->Add(bus);
Chassis->Add(new FloppyDisk(“3.5 in Floppy”));
Cout << “The net price is ” << chassis->NetPrice() << endl;
10. 已知应用
几乎在所有的面向对象的系统中都有Composite模式的应用实例。在SmallTalk中的Model、View、Controller结构中,原始View类即使一个Composite,几乎每个用户界面工具箱或框架都遵循这些步骤。
11. 相关模式
通常部件-父部件连接用于Responsibility of Chain模式。
Decorator模式经常和Compositor模式一起使用。当装饰和组合一起使用时,它们通常有一个公共的父类。因此装饰必须支持具有Add、Remove和GetChild操作的Component接口。
Flyweight可以共享组件,但不能再引用他们的父部件。
Iterator可用来遍历Composite。
Visitor将本来应该分布再Composite和Leaf类中的操作和行为局部化。