文章目录
四、结构型模式
结构型模式涉及到如何组合类和对象以获得更大的结构。结构型类模式采用继承机制来组合接口或实现。
-
一个简单的例子是采用多重继承方法将两个以上的类组合成一个类,结果这个类包含了所有父类的性质。这一模式尤其有助于多个独立开发的类库协同工作。
-
另外一个例子是类形式的Adapter(4.1)模式。一般来说 ,适配器使得一个接口(adaptee的接口)与其他接口兼容,从而给出了多个不同接口的统一抽象。 为此,类适配器对一个adaptee类进行私有继承。这样,适配器就可以用adaptee的接口表示它的接口。
结构型对象模式不是对接口和实现进行组合,而是描述了如何对一些对象进行组合, 从而实现新功能的一些方法。因为可以在运行时刻改变对象组合关系,所以对象组合方式具有更大的灵活性,而这种机制用静态类组合是不可能实现的。
Composite (4.3)模式是结构型对象模式的一个实例。它描述了如何构造-一个类层次式结构,这一结构由两种类型的对象(基元对象和组合对象)所对应的类构成.其中的组合对象使得你可以组合基元对象以及其他的组合对象,从而形成任意复杂的结构。
在Proxy (4.7) 模式中,proxy对象作为其他对象的一个方便的替代或占位符。它的使用可以有多种形式。例如它可以在局部空间中代表-个远程地址空间中的对象,也可以表示一个要求 被加载的较大的对象,还可以用来保护对敏感对象的访问。Proxy模式还提供了对对象的一些特有性质的一定程度上的间接访问,从而它可以限制、增强或修改这些性质。
Flyweight(4.6)模式为了共享对象定义了一个结构。至少有两个原因要求对象共享:效率和一致性。Flyweight的对 象共享机制主要强调对象的空间效率。使用很多对象的应用必需考虑每一个对象的开销。使用对象共享而不是进行对象复制,可以节省大量的空间资源。但是仅当这些对象没有定义与上下文相关的状态时,它们才可以被共享。Flyweight的对象没有 这样的状态。任何执行任务时需要的其他一些信息仅当需要时才传递过去。由于不存在与上下
文相关的状态,因此Flyweight对象可 以被自由地共享。
如果说Flyweight模式说明了如何生成很多较小的对象,那么Facade(4.5)模式则描述了如何用单个对象表示整个子系统。模式中的facade用来表示一组对象,facade的职责是将 消息转发给它所表示的对象。Bridge(4.2)模式将对象 的抽象和其实现分离,从而可以独立地改变它们。
Decorator(4.4)模式描述了如何动态地为对象添加职责。Decorator模式是一种结构型模式。这一模式采用递归方式组合对象,从而允许你添加任意多的对象职责。例如,一个包含用户界面组件的Decorator对象可以将边框或阴影这样的装饰添加到该组件中,或者它可以将窗口滚动和缩放这样的功能添加的组件中。我们可以将一个Decorator对象 嵌套在另外-个对象中就可以很简单地增加两个装饰,添加其他的装饰也是如此。因此,每个Decorator对象必须与其组件的接口兼容并且保证将消息传递给它。Decorator模式在转 发一条信息之前或之后都可以完成它的工作(比如绘制组件的边框)。
许多结构型模式在某种程度上具有相关性,我们将在本章末讨论这些关系。
对类:
- Adapter
对对象:
- Adapter
- Composite
- Proxy
- Flyweight
- Facade
- Decorator
- Bridge
4.1 ADAPTER (适配器)
1.意图
将一个类的接口转换成客户希望的另外-个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
2.别名
包装器Wrapper。
补充部分
此处补充对对象的适配器(对象适配器)。
// 目标接口(新接口)
class ITarget{
public:
virtual void process() = 0;
};
// 遗留接口(老接口)
class IAdaptee{
public:
virtual void foo(int data) = 0;
virtual int bar() = 0;
};
// 旧的实现
class OldClass: public IAdaptee{
//...
};
// 适配器
class Adapter:public ITarget{
protected:
IAdaptee* pAdaptee;
public:
Adapter(IAdaptee* pAdaptee){
this->pAdaptee = pAdaptee;
}
virtual void process(){
int data = pAdaptee->bar();
pAdaptee->foo(data);
}
};
// 使用
int main(){
IAdaptee * pAdaptee = new OldClass();
ITarget* pTarget = new Adapter(pAdaptee);
pTarget->process();
}
对象适配器和类适配器的区别在于:
对象适配器组合对象,类适配器多重继承类。
一般可以如下
class Adapter:public ITarget protected OldClass{
//...
};
3.动机
有时,为复用而设计的工具箱类不能够被复用的原因仅仅是因为它的接口与专业应用领域所需要的接口不匹配。
例如,有一个绘图编辑器,这个编辑器允许用户绘制和排列基本图元(线、多边型和正文等)生成图片和图表。这个绘图编辑器的关键抽象是图形对象。图形对象有一个可编辑的形状,并可以绘制自身。图形对象的接口由一个称为Shape的抽象类定义。
绘图编辑器为每一种图形对象定义了一个Shape的子类: LineShape类对 应于直线,PolygonShape类对应于多边型,等等。
像LineShape和PolygonShape这样的基本几何图形的类比较容易实现,这是由于它们的绘图和编辑功能本来就很有限。但是对于可以显示和编辑正文的TextShape子类来说,实现相当困难,因为即使是基本的正文编辑也要涉及到复杂的屏幕刷新和缓冲区管理。同时,成品的用户界面工具箱可能已经提供了一个复杂的TextView类用于显示和编辑正文。
理想的情况是我们可以复用这个TextView类以实现TextShape类,但是工具箱的设计者当时并没有考虑Shape的存在,因此TextView和Shape对象不能互换。
一个应用可能会有一些类具有不同的接口并且这些接口互不兼容,在这样的应用中象TextView这样已经存在并且不相关的类如何协同工作呢?
我们可以改变TextView类使它兼容Shape类的接口,但前提是必须有这个工具箱的源代码。
然而即使我们得到了这些源代码,修改TextView也是没有什么意义的;因为不应该仅仅为了实现一个应用,工具箱就不得不采用一些与特定领域相关的接口。
我们可以不用上面的方法,而定义一个TextShape类,由它来适配TextView的接口和Shape的接口。我们可以用两种方法做这件事:
- 继承Shape类的接口和TextView的实现
- 将一个TextView实例作为TextShape的组成部分,并且使用TextView的接口实现TextShape。
这两种方法恰恰对应于Adapter模式的类和对象版本。我们将TextShape称之为适配器Adapter。
上面的类图说明了对象适配器实例。它说明了在Shape类中声明的BoundingBox请求如何被转换成在TextView类中定义的GetExtent请求。由于TextShape将TextView的接口与Shape的接口进行了匹配,因此绘图编辑器就可以复用原先并不兼容的TextView类。
Adapter时常还要负责提供那些被匹配的类所没有提供的功能,上面的类图中说明了适配器如何实现这些职责。由于绘图编辑器允许用户交互的将每一-个Shape对象 “拖动”到一个新的位置,而TextView设计中 没有这种功能。我们可以实现TextShape类的CreateManipulator操作,从而增加这个缺少的功能,这个操作返回相应的Manipulator子类的一个实例。
Manipulator是一个抽象类, 它所描述的对象知道如何驱动Shape类响应相应的用户输人,例如将图形拖动到一个新的位置。对应于不同形状的图形,Manipulator有 不同的子类;例如子类TextManipulator对应于TextShape。
TextShape通过返 回一个TextManipulator实例,增加了TextView中缺少而Shape需要的功能。
4.适用性
以下情况使用Adapter模式
- 你想使用一个已经存在的类,而它的接口不符合你的需求。
- 你想创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类(即那些接口可能不一定兼容的类)协同工作。
- (仅适用于对象Adapter )你想使用一些已经存在的子类,但是不可能对每一个都进行子类化以匹配它们的接口。对象适配器可以适配它的父类接口。
5.结构
Target:是我们的希望的接口,也就是新的接口。
Adaptee:被试配者,以前的接口,遗留的接口。
我们通过Adapter完成这两个部分的转换。而不去改变这两个类。
类适配器使用多重继承对另一个接口与另一个类进行匹配
对象匹配依赖于对像组合,如下所示。
6.参与者
- Target (Shape)
- 定义Client使用的与特定领域相关的接口。
- Client (DrawingEditor)
- 与符合Target接口的对象协同。
- Adaptee (TextView)
- 定义一个已经存在的接口,这个接口需要适配。
- Adapter (TextShape)
- 对Adaptee的接口与Target接口进行适配
7.协作
- Client在Adapter实例上调用-些操作。接着适配器调用Adaptee的操作实现这个请求。
8.效果
类适配器和对象适配器有不同的权衡。
类适配器
- 用一个具体的Adapter类对Adaptee和Targei进行匹配。结果是当我们想要匹配一个类以及所有它的子类时,类Adapter将不 能胜任工作。
- 使得Adapter可以重定义Adaptee的部分行为,因为Adapter是Adaptee的一个子类。
- 仅仅引人了一个对象,并不需要额外的指针以间接得到adaptee。
对象适配器则
- 允许一个Adapter与多个Adaptee–即 Adapee本身以及它的所有子类(如果有子类的话)同时工作。Adapter也可 以一次给所有的Adaptee攀加功能。
- 使得重定义Adapee的行为比较困难。这就需要生成Adapee的子类并且使得Adapter引用这个子类而不是引用Adaptee本身。
使用Adapter模式时需要考虑的其他一些因素有:
- Adapter的匹配程度
对Adaptee的接口与Targt的接口进行匹配的工作量各个Adapter可能不一样。工作范围可能是,从简单的接口转换(例如改变操作名)到支持完全不同的操作集合。Adaptet的工作量取决于Target接口与Adaptee接口的相似程度。
- 可插入的Adapter
当其他的类使用一个类时,如果所需的假定条件越少,这个类就更具可复用性。如果将接口匹配构建为一个类,就不需要假定对其他的类可见的是一个相同的接口。也就是说,接口匹配使得我们可以将自己的类加人到一些现有的系统中去,而这些系统对这个类的接口可能会有所不同。Ojct-Work/Smalltalk使用pluggable adapter一词描述那些具有内部接口适配的类。
考虑TreeDisplay窗口组件,它可以图形化显示树状结构。如果这是一个具有特殊用途的窗口组件,仅在一个应用中使用,我们可能要求它所显示的对象有一个特殊的接口,即它们都是抽象类Tree的子类。
如果我们希望使TeeDisplay有具有良好的复用性的话(比如说,我们希望将它作为可用窗口组件工具箱的一部分),那么这种要求将是不合理的。应用程序将自己定义树结构类,而不应一定要使用我们的抽象类Tree。不同的树结构会有不同的接口。
例如,在一个目录层次结构中,可以通过GetSubdirectories操作进行访问子目录,然而在一个继承式层次结构中,相应的操作可能被称为GetSubclasses.尽管这两种层次结构使用的接口不同,一个可复用的TreeDisplay窗口组件必须能显示所有这两种结构。也就是说,TreeDisplay应具有接口适配的功能。
我们将在实现一节讨论在类中构建接口适配的多种方法。
- 使用双向适配器提供遣明操作
使用适配器的一个潜在问题是,它们不对所有的客户都透明。被适配的对象不再兼容Adaptee的接口,因此并不是所有Adaptee对象可以被使用的地方它都可以被使用。双向适配器提供了这样的透明性。在两个不同的客户需要用不同的方式查看同一个对象时,双向适配器尤其有用。
考虑一个双向适配器,它将图形编辑框架Unidraw与约束求解工具箱QOCA集成起来。这两个系统都有一些类,这些类显式地表示变量: Unidraw含有类StateVariable, QOCA中含有类ConstraintVariable,如下图所示。
为了使Unidraw与Q0CA协同工作,必须首先使类ConstraintVariable与类State’ Variable相匹配;而为了将QOCA的求解结果传递给Unidraw,必须使StateVariable 与ConstraintVariable相匹配。
这一方案中包含了一个双向适配器ConstraintStateVariable,它是类ConstraintVariable与类StateVariable共同的子类,ConstraintState Variable使得两个接口互相匹配。在该例中多重继承是一个可行的解决方案,因为被适配类的接口差异较大。双向适配器与这两个被匹配的类都兼容,在这两个系统中它都可以工作。
9.实现
尽管Adapter模式的实现方式通常简单直接,但是仍需要注意以下一些问题:
- 使用C++实现适配器类
在使用C++实现适配器类时, Adapter类应 该采用公共方式继承Target类,并且用私有方式继承Adaptee类。因此,Adapter类应该是Target的子类型,但不是Adaptee的子类型。
- 可插入的适配器
有许多方法可以实现可插人的适配器。例如,前面描述的TreeDisplay窗口组件可以自动的布置和显示层次式结构,对于它有三种实现方法:
首先,这也是所有这三种实现都要做的)是为Adaptee找到一个“窄”接口,即可用于适配的最小操作集。因为包含较少操作的窄接口相对包含较多操作的宽接口比较容易进行匹配。
对于TreeDisplay而言,被匹配的对象可以是任何一个层次式结构。因此最小接口集合仅包含两个操作:
- 一个操作定义如何在层次结构中表示一个节点
- 一个操作返回该节点的子节点。
对这个窄接口,有以下三个实现途径:
- 使用抽象操作在TreeDisplay类中定义窄Adaptee接口相应的抽象操作。
这样就由子类来实现这些抽象操作并匹配具体的树结构的对象。
例如,DirectoryTreeDisplay子 类将通过访问目录结构实现这些操作,如下图所示。
Directory’ TreeDisplay对这个窄接口加以特化,使得它的DirectoryBrowser客户可以用它来显示目录结构。
- 使用代理对象在这种方法中,TreeDisplay将访问树结构的请求转发到代理对象。
TreeDisplay的客户进行-些选择,并将这些选择提供给代理对象,这样客户就可以对适配加以控制,如下图所示。
例如,有一个DirectoryBrowser,它像前面一样使用TreeDisplay。DirectoryBrowser可 能为匹配TreeDisplay和层次目录结构构造出一个较好的代理。在Smalltalk或Objective C这样的动态类型语言中,该方法只需要一个接口对适配器注册代理即可。然后TreeDisplay简单地将请求转发给代理对象。NEXTSTEP大量使用这种方法以减少子类化。
在C++这样的静态类型语言中,需要一个代理的显式接口定义。我们将TreeDisplay需要的窄接口放入纯虚类TreeAccessorDelegate中,从而指定这样的一个接口。然后我们可以运用继承机制将这个接口融合到我们所选择的代理中一这 里我们选择DirectoryBrowser。如果DirectoryBrowser没有父类我们将采用单继承,否则采用多继承。这种将类融合在一起的方法相对于引人一个新的TreeDisplay子类并单独实现它的操作的方法要容易一些。
- 参数化的适配器
通常在Smalltalk中支持可插人适配器的方法是,用一个或多个模块对适配器进行参数化。模块构造支持无子类化的适配。一个模块可以匹配一个请求,并且适配器可以为每个请求存储一一个模块。
在本例中意味着,TreeDisplay存储的一个模块用来将一个节点转化成为一个GraphicNode,另外-一个模块用来存取一个节点的子节点。
例如,当对一个目录层次建立TreeDisplay时,我们可以这样写:
如果你在一一个类中创建接口适配,这种方法提供了另外- -种选择,它相对于子类化方法来说更方便一些。
10.代码示例
对动机一节中例子,从类Shape和TextView开始 ,我们将给出类适配器和对象适配器实现代码的简要框架。
class Shape {
public:
Shape() ;
virtual void BoundingBox (Point& bottomLeft, Point& topRight)const;
virtual Manipulator* CreateManipulator() const;
);
class TextView {
public:
TextView();
void GetOrigin(Coord& x,Coord& y) const;
void GetExtent (Coord& width, Coord& he1ght) const;
virtual bool IsEmpty () const;
};
Shape假定有一个边框, 这个边框由它相对的两角定义。而TextView则由原点、 寬度和高度定义。Shape同时定义了CreateManipulator操作用于创建一个Manipulator对象。 当用户操作一个图形时,Manipulator对象知道如何驱动这个图形TextView没有等同的操作。TextShape类是这些不同接口间的适配器。
类适配器采用多重继承适配接口。类适配器的关键是用一个分支继承接口,而用另外一个分支继承接口的实现部分。通常C++中作出这一区分的方法是:
用公共方式继承接口;用私有方式继承接口的实现。下面我们按照这种常规方法定义TextShape适配器。
class TextShape : public Shape,private TextView {
public:
Textshape();
virtual void BoundingBox (Point& bottonLeft,Point& topRight) const;
virtual bool IsEmpty() const;
virtua1 Manipulator* CreateManipulatorl) const;
};
BoundingBox操作对TextView的接口进行转换使之匹配Shape的接口。
vold TextShape::BoundingBox (Point& bottomLett, Point& topRight) const{
Coord bottom,left,width,height;
Getorigin(bottom, left);
GetExtent(width,height) ;
bottomLeft Point (bottom,left) :
topRight = Point (bottom + height, left + width)
}
IsEmpty操作给出了在适配器实现过程中常用的一种方法,直接转发请求:
bool TextShapeuiIsEmpty () const {
return TextView::IsEmpty();
}
最后,我们定义CreateManipulator ( TextView不支持该操作),假定我们已经实现了支持TextShape操作的类TextManipulator。
Manipulator* TextShape::CreateManipulator()const{
return new TextManipulator(this);
}
对象适配器采用对象组合的方法将具有不同接口的类组合在一起。在该方法中,适配器TextShape维护一个指向TextView的指针。
class TextShape : public Shape {
public:
Text Shape (TextView*);
virtual void BoundingBox{Point& bottomLeft, Point& topRight) const;
virtual bool IsEmpty() const ;
virtual Manipulator* CreateManipulator() const;
private:
TextView* text :
};
TextShape必须在构造器中对指向TextView实例的指针进行初始化,当它自身的操作被调用时,它还必须对它的TextView对象调用相应的操作。
在本例中,假设客户创建了TextView对象并且将其传递给TextShape的构造器:
TextShape::TextShape (TextView* t) {
_text = t;
}
void Textshape::BoundingBox(Point& bottomLeft, Point& topRight) const {
Coord bottom, left, width, height;
_text->GetOrigin (bottom, left);
_text->GetExtent (width, height);
bottomLeft = Point (bottom, left);
topRight = Point (bottom + height, left + width) ;
}
bool TextShape: :IsEmpty() const{
return text->IsEmpty() ;
}
CreateManipulator的实现代码与类适配器版本的实现代码-样,因为它的实现从零开始,没有复用任何TextView巳有的函数。
Manipulator* Text Shape::CreateManipulator () const{
return new TextManipulator (this);
}
将这段代码与类适配器的相应代码进行比较,可以看出编写对象适配器代码相对麻烦一些,但是它比较灵活。
例如,客户仅需将TextView子类的一个实例传给TextShape类的构造函数,对象适配器版本的TextShape就同样可以与TextView子类一起很好的工作。
11.相关模式
模式Bridge(4.2)的结构与对象适配器类似,但是Bridge模式的 出发点不同: Bridge目的是将接口部分和实现部分分离,从而对它们可以较为容易也相对独立的加以改变。而Adapter则意味着改变一个已有对象的接口。
Decorator(4.4)模式增强了其他对象的功能而同时又不改变它的接口。因此decorator对应用程序的透明性比适配器要好。结果是decorator支持递归组合,而纯粹使用适配器是不可能实现这一点的。
模式Proxy(4.7)在不改变它的接口的条件下,为另一个对象定义了一个代理。
4.2 BRIDGE (桥接)
1.意图
将抽象部分与它的实现部分分离,使它们都可以独立地变化。
2.别名
Handle/Body
补充部分
由于某些类型的固有实现逻辑,使得它们拥有两个变化的维度,乃至多个维度的变化。我们可以用桥接来帮助抽象它们。
现有如下场景,它有两个变化方向
- 平台会变
- 业务会变
我们如果正常一个一个写抽象的话,可能需要完成的类:平台抽象*业务抽象
class Messager{
public:
// 业务相关
virtual void Login(string username,string password) = 0;
virtual void SendMessage(string message) = 0;
virtual void SendPicture(Image image) = 0;
// 平台相关
virtual void PlaySound() = 0;
virtual void DrawShape() = 0;
virtual void WriteText() = 0;
virtual void Connect() = 0;
virtual ~Message(){}
};
// PC平台实现
class PCMessagerBase:public Messager{
public:
virtual void PlaySound(){
// ...
}
virtual void DrawShape(){
// ...
}
virtual void WriteText(){
// ...
}
virtual void Connect(){
// ...
}
};
// Mobile平台实现
class MobileMessagerBase:public Messager{
public:
virtual void PlaySound(){
// ...
}
virtual void DrawShape(){
// ...
}
virtual void WriteText(){
// ...
}
virtual void Connect(){
// ...
}
};
// 接下来,我们要针对不同版本、平台实现不同功能
// 业务抽象
class PCMessagerPerfect:public PCMessagerBase{
public:
virtual void Login(String username,string password){
PCMessagerBase::PlaySound();
PCMessagerBase::Connect();
// ...
}
virtual void SendMessage(string message){
PCMessagerBase::PlaySound();
PCMessagerBase::WriteText();
// ...
}
virtual void SendPicture(Image image){
PCMessagerBase::PlaySound();
PCMessagerBase::DrawShape();
// ...
}
};
class PCMessagerLite:public PCMessagerBase{
public:
virtual void Login(String username,string password){
PCMessagerBase::Connect();
// ...
}
virtual void SendMessage(string message){
PCMessagerBase::WriteText();
// ...
}
virtual void SendPicture(Image image){
PCMessagerBase::DrawShape();
// ...
}
};
我们很容易发现,业务抽象里面,有很多重复的地方。同时如果有Mobile、Pad等等,就会觉得很多重复代码的组合,非常麻烦。
于是我们使用桥接,来减少我们实际需要写的抽象数目。
将业务和平台分开抽象,来让我们需要写的抽象数目变为:平台抽象+业务抽象
class Messager{
protected:
MessageImp* messagerImp;
public:
virtual void Login(string username,string password) = 0;
virtual void SendMessage(string message) = 0;
virtual void SendPicture(Image image) = 0;
virtual ~Message(){}
};
// 由于上面我们是分开实现(业务和平台分开)的,因此此处我们也将它们拆开
class MessagerImp{
public:
virtual void PlaySound() = 0;
virtual void DrawShape() = 0;
virtual void WriteText() = 0;
virtual void Connect() = 0;
virtual ~MessagerImp(){}
};
// PC平台实现
class PCMessagerImp:public MessagerImp{
public:
virtual void PlaySound(){
// ...
}
virtual void DrawShape(){
// ...
}
virtual void WriteText(){
// ...
}
virtual void Connect(){
// ...
}
};
class MessagerPerfect:public Messager{
public:
virtual viod Login(String username,string password){
messagerImp->PlaySound();
messagerImp->Connect();
// ...
}
virtual void SendMessage(string message){
messagerImp->PlaySound();
messagerImp->WriteText();
// ...
}
virtual void SendPicture(Image image){
messagerImp->PlaySound();
messagerImp->DrawShape();
// ...
}
};
class MessagerLite:public Messager{
public:
virtual viod Login(String username,string password){
messagerImp->Connect();
// ...
}
virtual void SendMessage(string message){
messagerImp->WriteText();
// ...
}
virtual void SendPicture(Image image){
messagerImp->DrawShape();
// ...
}
};
void Process(){
// 运行时装配
MessagerImp* mImp = new PCMessagerImp();
Messager *m = new Messager(mImp);// 此处构造函数上面代码偷懒就没有写
}
3.动机
当一个抽象可能有多个实现时,通常用继承来协调它们。
抽象类定义对该抽象的接口,而具体的子类则用不同方式加以实现。但是此方法有时不够灵活。
继承机制将抽象部分与它的实现部分固定在一起,使得难以对抽象部分和实现部分独立地进行修改、扩充和重用。
让我们考虑在一个用户界面工具箱中,一个可移植的Window抽象部分的实现。
例如,这一抽象部分应该允许用户开发一些在X Window System和IBM的Presentation Manager(PM)系统中都可以使用的应用程序。运用继承机制,我们可以定义Window抽象类和它的两个子类XWindow与PMWindow,由它们分别实现不同系统平台上的Window界面。但是继承机制有两个不足之处:
- 扩展Window抽象使之适用于不同种类的窗口或新的系统平台很不方便。假设有Window的-个子类IconWindow,它专门将Window抽象用于图标处理。为了使IconWindow支持两个系统平台,我们必须实现两个新类XIconWindow和PMIconWindow,更为糟糕的是,我们不得不为每一种类型的窗口都定义两个类。而为了支持第三个系统平台我们还必须为每一种窗口定义一个新的Window子类,如下图所示。
- 继承机制使得客户代码与平台相关。每当客户创建-一个窗口时,必须要实例化一个具体的类,这个类有特定的实现部分。例如,创建Xwindow对象会将Window抽象与X Window的实现部分绑定起来,这使得客户程序依赖于X Window的实现部分。这将使得很难将客户代码移植到其他平台上去。
客户在创建窗口时应该不涉及到其具体实现部分。仅仅是窗口的实现部分依赖于应用运行的平台。这样客户代码在创建窗口时就不应涉及到特定的平台。
Bridge模式解决以上问题的方法是,将Window抽象和它的实现部分分别放在独立的类层次结构中。
其中一个类层次结构针对窗口接口( Window、IconWindow、TransientWindow ),另外一个独立的类层次结构针对平台相关的窗口实现部分,这个类层次结构的根类为WindowImp。
例如XwindowImp子类提供了一个基于X Window系统的实现,如下图所示。
对Window子类的所有操作都是用WindowImp接口中的抽象操作实现的。这就将窗口的抽象与系统平台相关的实现部分分离开来。因此,我们将Window与WindowImp之间的关系称之为桥接,因为它在抽象类与它的实现之间起到了桥梁作用,使它们可以独立地变化。
4.适用性
以下一些情况使用Bridge模式:
- 你不希望在抽象和它的实现部分之间有一个固定的绑定关系。例如这种情况可能是因为,在程序运行时刻实现部分应可以被选择或者切换。
- 类的抽象以及它的实现都应该可以通过生成子类的方法加以扩充。这时Bridge模式使你可以对不同的抽象接口和实现部分进行组合,并分别对它们进行扩充。
- 对一个抽象的实现部分的修改应对客户不产生影响,即客户的代码不必重新编译。
- (C++)你想对客户完全隐藏抽象的实现部分。在C++中,类的表示在类接口中是可见的。
- 正如在意图一节的第一个类图中所示的那样,有许多类要生成。这样-种类层次结构说明你必须将一个对象分解成两个部分。Rumbaugh称这种类层次结构为“嵌套的普化”( nested generalizations )。
- 你想在多个对象间共享实现(可能使用引用计数),但同时要求客户并不知道这一点。
一个简单的例子便是Coplien的String类,在这个类中多个对象可以共享同一个字符串表示( StringRep )。
5.结构
6.参与者
- Abstraction (Window)
- 定义抽象类的接口。
- 维护一个指向Implementor类型对象的指针。
- RefinedAbstraction (IconWindow)
- 扩充由Abstraction定义的接口。
- Implementor (WindowImp)
- 定义实现类的接口,该接口不一定要与Asbstraction的接口完全一致; 事实上这两个接口可以完全不同。一般来讲,Implementor接 口仅提供基本操作,而Abstraction则定义了基于这些基本操作的较高层次的操作。
- ConcreteImplementor (XwindowImp, PMWindowImp)
- 实现Implementor接口并定义它的具体实现。
7.协作.
- Abstraction将client的请求转发给它的Implementor对象。
8.效果
Bridge模式有以下一些优点:
- 分离接口及其实现部分
一个实现未必不变地绑定在一个接口上。 抽象类的实现可以在运行时刻进行配置,一个对象甚至可以在运行时刻改变它的实现。
将Abstraction与Implementor分离有助于降低对实现部分编译时刻的依赖性,当改变一个实现类时,并不需要重新编译Abstraction类和它的客户程序。为了保证一个类库的不同版本之间的二进制兼容性,一定要有这个性质。
另外,接口与实现分离有助于分层,从而产生更好的结构化系统,系统的高层部分仅需知道Astraction和Implementor即可。
- 提高可扩充性
你可以独立地对Abstraction和Implementor层次结构进行扩充。
- 实现细节对客户透明
你可以对客 户隐藏实现细节,例如共享Implementor对象以及相应的引用计数机制(如果有的话)。
9.实现
使用Bridge模式时需要注意以下一些问题:
- 仅有一个Implementor
在仅有 一个实现的时候,没有必要创建-个抽象的Implementor类。这是Bridge模式的退化情况;在Abstraction与Implementor之间有一种一对一的关系。尽管如此,当你希望改变一个类的实现不会影响已有的客户程序时,模式的分离机制还是非常有用的也就是说,不必重新编译它们,仅需重新连接即可。
Carolan用“常露齿嘻笑的猫”( Cheshire Cat)描述这一分离机制。在C++中,
Implementor类的类接口可以在一个私有的头文件中定义,这个文件不提供给客户。这样你就对客户彻底隐藏了一个类的实现部分。
- 创建正确的Implementor对象
当存在多个Implementor类的时候,你应该用何种方法,在何时何处确定创建哪一个Implementor类呢?
如果Abstraction知道所有的ConcreteImplementor类,它就可以在它的构造器中对其中的一个类进行实例化,它可以通过传递给构造器的参数确定实例化哪一个类。
例如,如果一个collection类支持多重实现,就可以根据collction的大小决定实例化哪一个类。
链表的实现可以用于较小的ollection类,而hash表则可用于较大的Collection类。
另外有一种方法是首先选择一个缺省的实现, 然后根据需要改变这个实现。例如,如果一个collection的大小超出了一定的阈值时,它将会切换它的实现,使之更适用于表目较多的collection。
也可以代理给另一个对象,由它一次决定。在Window/WindowImp的例子中, 我们可以引人一个factory对象(参见Abstract Factory(3.1)), 该对象的唯一职责就是封装系统平台的细节。这个对象知道应该为所用的平台创建何种类型的WindowImp对象; Window仅需向它请求一个WindowImp,而它会返回正确类型的WindowImp对象。这种方法的优点是Abstraction类不和任何一个Implementor类直接耦合。
- 共享Implementor对象
Coplien阐明 了如何用C+ +中常用的Handle/Body方法在多个对象间共享一些实现。其中Body有一个对象引用计数器,Handle对它进行增减操作。将共
享程序体赋给句柄的代码一般具有以下形式:
Handle& Handle::operator = (const Handle& other) {
other._ body->Ref();
_body->Unref();
if (_ body->RefCount() = 0) {
delete_ body;
}
_body = other.. body;
return *this;
}
- 采用多重继承机制
在C++中 可以使用多重继承机制将抽象接口和它的实现部分结合起来。例如,一个类可以用public方式继承Abstraction而以private方式继承
ConcreteImplementor。但是由于这种方法依赖于静态继承,它将实现部分与接口固定不变的绑定在一起。 因此不可能使用多重继承的方法实现真正的Bridge模式——至少用C++不行。
10.代码示例
下面的C++代码实现了意图一节中Window/WindwoImp的例子,其中Window类为客户应用程序定义了窗口抽象类:
class Window {
public:
Window (View* contents) ;
// requests hand1ed by window
virtua1 void DrawContents() ;
virtual void Open() ;
virtual void Close();
virtual void Iconify() ;
virtual void Deiconify() ;
// requests forwarded to impl ementation
virtual void SetOrigin (const Point& at) ;
virtual void SetExtent (const Point& extent) ;
virtual void Raise() ;
virtual void Lower() ;
virtual void DrawLine (const Point&,const Point&);
virtual void DrawRect (const Point&, conat Point&) ;
virtual void DrawPolygon const(Point[], int n);
virtual void DrawText (const char*, const Point&)
protected:
WindowImp* GetwindowImp();
View* GetViewl:
private:
WindowImp* _imp;
View* _contents; // the window's contents
Window维护一个对WindowImp的引用,WindowImp抽象类定义了一个对底层窗口系统的
接口。
class windowImp (
public;
virtual void ImpTop() = 0;
virtual void ImpBottom() = 0;
virtual void ImpsetExtent (const Point&) = 0;
virtual void Impsetorigin (const Point&) = 0;
virtual void DeviceRect (Coord, Coord, Coord, Coord) = 0;
virtual void DeviceText (const char", Coord, Coord) = 0:
virtual vold DeviceBitmaplconst char*. Coord, Coord) = 0;
// lots more functiona for drawing on windows...
protected:
WindowImp();
};
Window的子类定义了应用程序可能用到的不同类型的窗口,如应用窗口、图标、对话框
临时窗口以及工具箱的移动面板等等。
例如ApplicationWindow类将实现DrawContents操作以绘制它所存储的View实例:
class ApplicationWindow:public Window {
public:
// ...
virtual void DrawContents{);
};
void Appl icationMindow::DrawCentents () {
GetViewl()->DrevOn(this);
}
IconWindow中存储了它所显示的图标对应的位图名
class IconWindow:public Window [
public:
// ...
virtual vo1d DramContents();
private:
const char* _bitmapNane;
};
.并且实现DrawContents操作将这个位图绘制在窗口上:
void Ieconwindow::DrawContents() (
windowInp* imp = GetWindowImp();
if(imp != 0){
imp->DeviceBitmap(_bitmapName, 0.0,0.0);
}
}
我们还可以定义许多其他类型的Window类,例如TransientWindow在 与客户对话时由一个窗口创建,它可能要和这个创建它的窗口进行通信; PaletteWindow 总是在其他窗口之上;IconDockWindow拥有一些IconWindow,并且由它负责将它们排列整齐。
Window的操作由WindowImp的接口定义。例如,在调用WindowImp操作在窗口中绘制矩形之前,DrawRect必须从它的两个Point参数中提取四个坐标值:
void Window::DrawRect (const Point& p1, const Point& p2) {
WindowImp* imp GetWindowImp();
imp->DeviceRect (p1.X(),p1.Y(), p2.x(), P2.Y());
}
具体的WindowImp子类可支持不同的窗口系统, XwindowImp子类 支持X Window窗口系统:
class xWindowImp:public WindowImp (
public:
XwindowImp();
virtual void DeviceRect (Coord, Coord, Coord, Coord) ;
// remainder of public interface...
private:
// lots of x window system-specific state, including:
Display* dpy:
Drawable_ winid; // window id
GC gc; // window graphic context
)
对于Presentation Manager (PM),我们定义PMWindowImp类:
class PMWindowImp:public WindowImp{
public:
PMWindowImp();
virtual void DeviceRect (Coord, Coord, Coord, Coord);
// remainder of public interface...
private:
// lots of PM window system-specific state, including:
HPS_ hps;
};
这些子类用窗口系统的基本操作实现WindowImp操作,例如,对于X窗口系统这样实现DeviceRect:
void xwindowImp::DeviceRect(Coord x0,Coord y0, Coord x1, Coord y1){
int x = round(min(x0,x1));
int y = round(min(y0, y1));
int w =*round(abs(x0 - x1)):
int h = round(abs(y0 - y1));
XDrawRectang1el(_dpy,_winid,_gc,x,y, w,h);
}
PM的实现部分可能象下面这样:
void PMWindowImp: : DeviceRect (
Coord x0,Coord y0,Coord x1, Coord y1;
Coord left = min(x0, :x1);
Coord right = max(x0,x1);
Coord bottom = min(y0, y1);
Coord top = max(y0,y1;
PPOINTL point[4];
point[0].x = 1eft;
point[0l.y = top;
point[1].x = right;
point[1].y = top;
point[2].x = right;
point[2].y = bottom;
point[3].x = 1eft;
point[3].y = bottom;
if (
(GpiBeginPath(_ hps,1L) == false) ||
(GpisetCurrentPosition(_ hps, &point[3]) = false)||
(GpiPolyLine(_hp8, 4L,point) == GPI_ ERROR)||
(GpiEndPath(_hps) false)
){
// report error
}
else
GpiStrokePath(_ hpg, 1L,0L);
}
那么一个窗口怎样得到正确的WindowImp子类的实例呢?
在本例我们假设Window类具有这个职责,它的GetWindowImp操作负责从一个抽象工厂(参见Abstract Factory(3.1)模式)得到正确的实例,这个抽象工厂封装了所有窗口系统的细节。
WindowImp* Window::GetWindowImp(){
if(_imp==0){
_imp = WindowSystemFactory::Instance()->MakeWindowImp();
}
return_ imp;
}
WindowSystemFactory::Instance()函数返回一个抽象工厂,该工厂负责处理所有与特定窗口系统相关的对象。
为简化起见,我们将它创建一个单件( Singleton),允许Window类直接访问这个工厂。
11.相关模式
Abstract Factory(3.1)模式可以用来创建和配置一个特定的Bridge模式。
Adapter(4.1)模式用来帮助无关的类协同工作,它通常在系统设计完成后才会被使用。然而,Bridge模式则是在系统开始时就被使用,它使得抽象接口和实现部分可以独立进行改变。
4.3 COMPOSITE(组合)
1.意图
将对象组合成树形结构以表示 “部分-整体” 的层次结构。Composite使得用户对单个对象和组合对象的使用具有一致性。
补充部分
class Component{
public:
virtual void process() = 0;
virtual ~Component(){}
};
// 树节点
class Composite:public Component{
private:
string name;
list<Component*> elements;
public:
Composite(const string&s):name(s){}
void add(Component* element){
elements.push_back(element);
}
void remove(Component* element){
elements.remove(element);
}
void process(){
// 处理当前节点
// ...
// 处理子节点
for(auto &e:elements){
e->process();// 多态调用。此时,它可以是一个叶,也可以是一个树。
}
}
};
// 叶子节点
class Leaf:public Comonent{
private:
string name;
public:
Leaf(string s):name(s){}
void process(){
// 处理当前节点
}
};
void Invoke(Component& c){
// ...
c.process();
// ...
}
int main(){
Composite root("root");
Composite treeNode1("treeNode1");
Composite treeNode2("treeNode2");
Composite treeNode3("treeNode3");
Composite treeNode4("treeNode4");
Leaf leaf1("leaf1");
Leaf leaf2("leaf2");
root.add(&treeNode1);
treeNode1.add(&treeNode2);
treeNode2.add(&leaf1);
root.add(&treeNode3);
treeNode3.add(&treeNode4);
treeNode4.add(&leaf2);
process(root);
process(leaf2);
process(treeNode3);
}
2.动机
在绘图编辑器和图形捕捉系统这样的图形应用程序中,用户可以使用简单的组件创建复杂的图表。用户可以组合多个简单组件以形成一些较大的组件,这些组件又可以组合成更大的组件。一个简单的实现方法是为Text和Line这样的图元定义一些类,另外定义一些类作为这些图元的容器类(Container)。
然而这种方法存在一个问题:
使用这些类的代码必须区别对待图元对象与容器对象,而实际上大多数情况下用户认为它们是一样的。对这些类区别使用,使得程序更加复杂。Composite模式描述了如何使用递归组合,使得用户不必对这些类进行区别,如下图所示。
Composite模式的关键是一个抽象类,它既可以代表图元,又可以代表图元的容器。在图形系统中的这个类就是Graphic,它声明-一些与特定图形对象相关的操作,例如Draw。同时它也声明了所有的组合对象共享的一些操作,例如一些操作用于访问和管理它的子部件。
子类Line、Rectangle和Text(参见前面的类图)定义了一些图元对象,这些类实现Draw,分别用于绘制直线、矩形和正文。由于图元都没有子图形,因此它们都不执行与子类有关的操作。
Picture类定义了一个Graphic对象的聚合。Picture 的Draw操作是通过对它的子部件调用Draw实现的,Picture还用这种方法实现了一些与其子部件相关的操作。由于Picture接口与Graphic接口是一致的,因此Picture对象可以递归地组合其他Picture对象。
下图是一个典型的由递归组合的Graphic对象组成的组合对象结构。
3.适用性
以下情况使用Composite模式:
- 你想表示对象的部分-整体层次结构。
- 你希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。
4.结构
典型的Composite结构如下
5.参与者
Component (Graphic)
- 为组合中的对象声明接口。
- 在适当的情况下,实现所有类共有接口的缺省行为。
- 声明一个接口用于访问和管理Component的子组件。
- (可选)在递归结构中定义一个接口,用于访问一个父部件,并在合适的情况下实现它。
Leaf (Rectangle、Line、Text等)
- 在组合中表示叶节点对象,叶节点没有子节点。一在组合中定义图元对象的行为。
Composite (Picture)
- 定义有子部件的那些部件的行为。一存储子部件。
- 在Component接口中实现与子部件有关的操作。.
Client
- 通过Component接口操纵组合部件的对象。
6.协作
- 用户使用Component类接口与组合结构中的对象进行交互。如果接收者是一个叶节点,则直接处理请求。如果接收者是Composite,它通常将请求发送给它的子部件,在转发请求之前与/或之后可能执行–些辅助操作。
7.效果
Composite模式
- 定义了包含基本对象和组合对象的类层次结构
基本对象可以被组合成更复杂的组合对象,而这个组合对象又可以被组合,这样不断的递归下去。客户代码中,任何用到基本对象的地方都可以使用组合对象。
- 简化客户代码
客户可以一致地使用组合结构和单个对象。通常用户不知道(也不关心)处理的是一个叶节点还是一个组合组件。这就简化了客户代码,因为在定义组合的那些类中不需要写一些充斥着选择语句的函数。
- 使得更容易增加新类型的组件
新定义的Composite或Leaf子类自动地与已有的结构和客户代码一起工作,客户程序不需因新的Component类而改变。
- 使你的设计变得更加一般化
容易增加新组件也会产生一些问题,那就是很难限制组合中的组件。有时你希望一个组合只能有某些特定的组件。使用Composite时,你不能依赖类型系统施加这些约束,而必须在运行时刻进行检查。
8.实现
我们在实现Composite模式时需要考虑以下几个问题:
- 显式的父部件引用
保持从子部件到父部件的引用能简化组合结构的遍历和管理。父部件引用可以简化结构的上移和组件的删除,同时父部件引用也支持Chain of Responsibility(5.2)模式。
通常在Component类中定义父部件引用。Leaf和Composite类可以继承这个引用以及管理这个引用的那些操作。
对于父部件引用,必须维护一个不变式,即一个组合的所有子节点以这个组合为父节点,而反之该组合以这些节点为子节点。保证这一点最容易的办法是,仅当在一个组合中增加或删除一个组件时,才改变这个组件的父部件。
如果能在Composite类的Add和Remove操作中实现这种方法,那么所有的子类都可以继承这一方法,并且将自动维护这一不变式。
- 共享组件
惇共享组件是很有用的,比如它可以减少对存贮的需求。但是当一个组件只有一个父部件时,很难共享组件。
一个可行的解决办法是为子部件存贮多个父部件,但当一个请求在结构中向上传递时,这种方法会导致多义性。Flyweight(4.6)模式讨论了如何修改设计以避免将父部件存贮在一起的方法。如果子部件可以将一些状态(或是所有的状态)存储在外部,从而不需要向父部件发送请求,那么这种方法是可行的。
- 最大化Component接口
Composite模式的目的之一是使得用户不知道他们正在使用的具体的Leaf和Composite类。为了达到这一-目的,Composite类应为Leaf 和Composite类尽可能多定义一些公共操作。Composite类通常为这些操作提供缺省的实现,而Leaf 和Composite子类可以对它们进行重定义。
然而,这个目标有时可能会与类层次结构设计原则相冲突,该原则规定:一个类只能定义那些对它的子类有意义的操作。有许多Component所支持的操作对Leaf类似乎没有什么意义,那么Component怎样为它们提供一个缺省的操作呢?
有时一点创造性可以使得一个看起来仅对Composite才有意义的操作,将它移入Component类中,就会对所有的Component都适用。例如,访问子节点的接口是Composite类的一个基本组成部分,但对Leaf类来说并不必要。但是如果我们把一个Leaf看成一个没有子节点的Component,就可以为在Component类中定义一个缺省的操作,用于对子节点进行访问,这个缺省的操作不返回任何一个子节点。Leaf类可以使用缺省的实现,而Composite类则会重新实现这个操作以返回它们的子类。
管理子部件的操作比较复杂,我们将在下一项中予以讨论。
- 声明管理子部件的操作
虽然Composite类实现了Add和Remove操作用于管理子部件,但在Composite模式中一个重要的问题是:在Composite类层次结构中哪一些类声明这些操作。我们是应该在Component中声明这些操作,并使这些操作对Leaf类有意义呢,还是只应该在Composite和它的子类中声明并定义这些操作呢?
这需要在安全性和透明性之间做出权衡选择。
- 在类层次结构的根部定义子节点管理接口的方法具有良好的透明性,因为你可以一致地使用所有的组件,但是这一方法是以安全性为代价的,因为客户有可能会做一些无意义的事情,例如在Leaf中增加和删除对象等。
- 在Composite类中定义管理子部件的方法具有良好的安全性,因为在象C++这样的静态类型语言中,在编译时任何从Leaf中增加或删除对象的尝试都将被发现。但是这又损失了透明性,因为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;
if(test = aComponenE->Getcomposite()) {
test->Add(new Leaf):
}
acomponent = aLeaf;
if (test = acomponent->Getcomposite()){
test->Add(new Leaf); // will not add leaf
}
你可使用C++中的dynamic_.cast结构对Composite做相似的试验。
当然,这里的问题是我们对所有的组件的处理并不一致。在进行适当的动作之前,我们必须检测不同的类型。
提供透明性的唯一方法是在Component中定义缺省Add和Remove操作。这又带来了一个新的问题:Component::Add的实现不可避免地会有失败的可能性。
你可以不让Component::Add做任何事情,但这就忽略了一个很重要的问题:
- 企图向叶节点中增加一些东西时可能会引入错误。这时Add操作会产生垃圾。你可以让Add操作删除它的参数,但可能客户并不希望这样。
如果该组件不允许有子部件,或者Remove的参数不是该组件的子节点时,通常最好使用缺省方式(可能是产生一个异常)处理Add和Remove的失败。
另一个办法是对“删除”的含义作一些改变。如果该组件有一个父部件引用,我们可重新定义Component :: Remove,在它的父组件中删除掉这个组件。然而,对应的Add操作仍然没有合理的解释。
- Component是否应该实现一个Component列表
你可能希望在Component类中将子节点集合定义为一个实例变量,而这个Component类中也声明了一些操作对子节点进行访问和管理。但是在基类中存放子类指针,对叶节点来说会导致空间浪费,因为叶节点根本没有子节点。只有当该结构中子类数目相对较少时,才值得使用这种方法。
- 子部件排序
许多设计指定了Composite的子部件顺序。在前面的Graphics例子中,排序可能表示了从前至后的顺序。如果Composite表示语法分析树,Composite子部件的顺序必须反映程序结构,而组合语句就是这样一些Composite的实例。
如果需要考虑子节点的顺序时,必须仔细地设计对子节点的访问和管理接口,以便管理子节点序列。Iterator模式(5.4)可以在这方面给予一些定的指导。
- 使用高速缓冲存贮改善性能
如果你需要对组合进行频繁的遍历或查找,Composite类可以缓冲存储对它的子节点进行遍历或查找的相关信息。Composite可以缓冲存储实际结果或者仅仅是–些用于缩短遍历或查询长度的信息。例如,动机–节的例子中Picture类能高速缓冲存贮其子部件的边界框,在绘图或选择期间,当子部件在当前窗口中不可见时,这个边界框使得Picture不需要再进行绘图或选择。
一个组件发生变化时,它的父部件原先缓冲存贮的信息也变得无效。在组件知道其父部件时,这种方法最为有效。因此,如果你使用高速缓冲存贮,你需要定义一个接口来通知组合组件它们所缓冲存贮的信息无效。
- 应该由谁删除Component
在没有垃圾回收机制的语言中,当-一-个Composite被销毁时,通常最好由Composite负责删除其子节点。但有-种情况除外,即Leaf对象不会改变,因此可以被共享。
- 存贮组件最好用哪一种数据结构
Composite可使用多种数据结构存贮它们的子节点,包括连接列表、树、数组和hash表。数据结构的选择取决于效率。事实上,使用通用数据结构根本没有必要。有时对每个子节点,Composite都有一个变量与之对应,这就要求Composite的每个子类都要实现自己的管理接口。参见Interpreter(5.3)模式中的例子。
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 Iterator<Equipment*>*createIterator ();
protected:
Equipment (const char*);
private:
const char*_name;
};
Equipment声明一些操作返回一个设备的属性,例如它的能量消耗和价格。子类为指定的设备实现这些操作,Equipment还声明了一个CreateIterator操作,该操作为访问它的零件返回一个Iterator。这个操作的缺省实现返回一个NullIterator,它在空集上叠代。
Equipment的子类包括表示磁盘驱动器、集成电路和开关的Leaf类:
class PloppyDisk : 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<Equipment*>createiterator();
protected:
compositeEquipment(const char*);
private:
List<Equipment*> _equipment;
};
CompositeEquipment为访问和管理子设备定义了一些操作。操作Add和Remove从存储在_equipment成员变量中的设备列表中插人并删除设备。操作CreateIterator返回一个迭代器( ListIterator的一-个实例)遍历这个列表。
NetPrice的缺省实现使用CreateIterator来累加子设备的实际价格。
Currency CompositeEquipnent::NetPrice(){
Iterator<Equipment*>* i = createIterator();
Currency total = 0;
for(i->First();!i->IsDone();i->Next()){
total += i->currentItem()->NetPrice();
}
delete i;
return total;
}
现在我们将计算机的底盘表示为CompositeEquipment的子类Chassis。Chassis从CompositeEquipment继承了与子类有关的那些操作。
class chassis : public compositeEquipment {
public:
chassis(const char*);
virtual ~Chassis();
virtual Watt Power ();
virtual Currency NetPrice();
virtua1 Currency DiscountPrice() ;
};
我们可用相似的方式定义其他设备容器,如Cabinet和Bus。这样我们就得到了组装一台(非常简单)个人计算机所需的所有设备。
Cabinet* cabinet = new Cabinet("PC cabinet");
Chassis* chassis = new Chassis("PC chassis");
cabinet->Add(chassis);
Bus*bus = new Bus("MCA Bus");
bus->Add(new Card( "16Mbs Token Ring" ));
chassis->Add(bus);
chassis->Add(new FloppyDisk( "3.5in. Floppy" ));
cout << "The net price is "<< chassis->NetPrice() << endl;
10.相关模式
通常部件-父部件连接用于Responsibility of Chain(5.1)模式。
Decorator ( 4.4)模式经常与Composite模式一起使用。当装饰和组合一起使用时,它们通常有一个公共的父类。因此装饰必须支持具有Add、Remove和GetChild 操作的Component接口。
Flyweight(4.6)让你共享组件,但不再能引用他们的父部件。Itertor(5.4)可用来遍历Composite。
Visitor(5.11)将本来应该分布在Composite和Leaf类中的操作和行为局部化。
资料
[1]《设计模式:可复用面向对象软件的基础》(美) Erich Gamma Richard Helm、Ralph Johnson John Vlissides 著 ; 李英军、马晓星、蔡敏、刘建中 等译; 吕建 审校