一、功能
合成模式有时又叫做部分-整体模式(Part-Whole)。合成模式将对象组织到树结构中,可以用来描述整体与部分的关系。合成模式可以使客户端将单纯元素与复合元素同等看待。
从和尚的故事谈起
这是小时候我奶奶讲的故事:从前有个山,山里有个庙,庙里有个老和尚在给小和尚讲故事,讲的什么故事呢?从前有个山,山里有个庙……。奶奶的故事要循环多少次,根据你多长时间睡着而定。在故事中有山、有庙、有和尚、有故事。因此,故事的角色有两种:一种里面没有其它角色;另一种内部有其它角色。
对象的树结构
一个树结构由两种节点组成:树枝节点和树叶节点。树枝节点可以有子节点,而一个树叶节点不可以有子节点。除了根节点外,其它节点有且只有一个父节点。
注意:一个树枝节点可以不带任何叶子,但是它因为有带叶子的能力,因此仍然是树枝节点,而不会成为叶节点。一个树叶节点永远不可能带有子节点。
二、结构图
上图中,也可以做些扩展,根据需要可以将Leaf和Composite做为抽象基类,从中派生出子类来。
可以看出,上面的类图结构涉及到三个角色:
- 抽象构件(Component)角色:这是一个抽象角色,它给参与组合的对象规定一个接口。这个角色给出共有接口及其默认行为。
- 树叶构件(Leaf)角色:代表参加组合的树叶对象。一个树叶对象没有下级子对象。
- 树枝构件(Composite)角色:代表参加组合的有子对象的对象,并给出树枝构件对象的行为。
可以看出,Composite类型的对象可以包含其它Component类型的对象。换而言之,Composite类型对象可以含有其它的树枝(Composite)类型或树叶(Leaf)类型的对象。
合成模式的实现根据所实现接口的区别分为两种形式,分别称为安全模式和透明模式。合成模式可以不提供父对象的管理方法,但合成模式必须在合适的地方提供子对象的管理方法(诸如:add、remove、getChild等)。
抽象基类:
1)Component:为组合中的对象声明接口,声明了类共有接口的缺省行为(如这里的Add,Remove,GetChild函数),声明一个接口函数可以访问Component的子组件。
接口函数:
1)Component::Operatation:定义了各个组件共有的行为接口,由各个组件的具体实现。
2)Component::Add添加一个子组件
3)Component::Remove::删除一个子组件。
4)Component::GetChild:获得子组件的指针。
解析:
Component模式是为解决组件之间的递归组合提供了解决的办法,它主要分为两个派生类,其中的Leaf是叶子结点,也就是不含有子组件的结点,而Composite是含有子组件的类。举一个例子来说明这个模式,在UI的设计中,最基本的控件是诸如Button,Edit这样的控件,相当于是这里的Leaf组件,而比较复杂的控件比如List则可也看做是由这些基本的组件组合起来的控件,相当于这里的Composite,它们之间有一些行为含义是相同的,比如在控件上作一个点击,移动操作等等的,这些都可以定义为抽象基类中的接口虚函数,由各个派生类去实现之,这些都会有的行为就是这里的Operation函数,而添加,删除等进行组件组合的操作只有非叶子结点才可能有,所以虚拟基类中只是提供接口而且默认的实现是什么都不做。
三、优缺点
优点:对于Composite模式,也许人们一开始的注意力会集中在它是如何实现组合对象的。但Composite最重要之处在于用户并不关心是组合对象还是单个对象,用户将以统一的方式进行处理,所以基类应是从单个对象和组合对象中提出的公共接口。
缺点:Composite最大的问题在于不容易限制组合中的组件。
四、实现
话说,宋江带人攻陷东平东昌两郡,收降了双枪将董平和没羽箭张清,而后皇甫端来归。梁山聚集了武将,书生,医生以至兽医等各色人等,成为了一个成熟的社区。着点人数正好一百单八人,是个吉利数字,为此宋江组织了浩大的祭天仪式。在祭天仪式进行期间,发现了埋于地下的石碣,石碣上排定了三十六天罡,七十二地煞的座次。次序以定,梁山也开始走上正规,开始划分各位好汉的职责。依据各人特长,分别任职于马军,步军,军师等角色。有了规矩,才成方圆,自此梁山告别了单挑的时代,作战有了一些固定的组合。如李逵出战,必有项充,李衮护在身边;这位头脑简单的杀人魔王的典型特点是攻强守弱,而且不着盔甲,如若无人保护估摸早死了。还有另外一些组合:如矮脚虎王英和一丈青扈三娘这样的夫妻组合(扈三娘嫁给了王英,你能想通吗?我想不通);还有解宝解珍这样的兄弟组合等。来考虑一下,如何实现这样的功能,武将可以单独出击,也可以成群的出击?该如何去实现这样的功能呢?还是一步一步来,先看类图:
这是我们建立的武将类,该类和我们第一讲《 赵子龙单骑救主 》中建立的武将类相同(为了突出中心功能,简化了功能)。考虑一下在这种情况下,我们如果想让多个武将一起出击,该如何实现?最单纯的做法就是在调用该类的地方生成一个数组,然后客户端通过该数组来调用各个对象的操作。大概代码如下:
General general[10];
for( int i = 0; i<10; i++ )
general[i].Assault();
客户端也可以将这些操作封装在不同的函数中,但如果武将类填加新的函数的时候,客户端也需要相应的增加此类德函数。这是典型的封装不好的例子,客户端需要增加特殊的函数来支持对象。我自己的工作中就遇到了这样的问题,类没有封装好,针对于这些类的操作,你不得不填加一些函数,接着就可以发现在不同的文件里有很多功能相似的函数(一个类往往在多处用到,而这些往往是由不同的人写的,这样很容易造成重复代码)。整个程序被改得乱码七糟的,改这样的东西真是头痛万分,更加上你如果要用到这些外部的函数的时候,都不知道用那个。那如何来解决该问题呢?找到组合模式,就找到了解决之道。先来看看组合模式的类图描述:
Component是为Leaf和Composite抽象出来的共同接口,可以通过它来统一操作单个对象或者组合对象。
Leaf是我们具体要用到的类,它用来完成具体的功能。Composite是我们用来组合对象的类,并实现对子对象的管理;它通过Add,Remove和GetChild来实现对子对象的管理。Composite中会含有一个Component型的对象列表childers,它也会重载Leaf中所需要的函数,然后通过遍历执行所有childer的函数。该childer也可以是Composite型的,这样就可以有一个递归的结构。
我们用Composite模式来重新设计我们的类图,如下:
理解模式最有效的方式,还是看具体的代码:
#include
#include
#include
#include
using namespace std;
//抽象部件类,是Leaf和Composite的共有接口
class GeneralComponent
{
public:
//为了确保调用到析构函数,此处声明为虚函数
virtual ~GeneralComponent(){}
//由于General中不实现Add,Remove,GetChild,此处需要一个缺省的实现,但此处抛出异常更合适一些,为了简单,省略了。
virtual void Add(GeneralComponent* pComonent) {}
virtual void Remove(GeneralComponent* pComonent) {}
virtual GeneralComponent* GetChild(int i) {return NULL;}
virtual void Assault() = 0;
};
//具体用到的类
class General : public GeneralComponent
{
private:
string m_strName;
public:
General(string strName):m_strName(strName){}
void Assault()
{
cout << m_strName << " 进入战斗!" << endl;
}
};
//组合类
class GeneralComposite : public GeneralComponent
{
private:
string m_strName;
//用来存放需要组合的对象
vector m_pComponents;
public:
GeneralComposite(string strName):m_strName(strName){}
virtual ~GeneralComposite()
{
vector::iterator pos;
for( pos = m_pComponents.begin();pos::iterator ivite=
find(m_pComponents.begin(),m_pComponents.end(),pGeneral);
m_pComponents.erase(ivite);
}
GeneralComponent* GetChild(int i)
{
return m_pComponents[i];
}
void Assault()
{
cout << m_strName << "战斗序列" << endl;
//需要调用所有的组合对象的操作
vector::iterator pos;
for( pos = m_pComponents.begin();posAssault();
}
}
};
我们再来看一下,客户端如何来调用该对象:
int main(int argc, char* argv[])
{
GeneralComposite pArmy("梁山大军");
GeneralComposite* pHorseArmy = new GeneralComposite("梁山马军");
GeneralComposite* pPaceArmy = new GeneralComposite("梁山步军");
General *pWusong = new General("好汉武松");
General *pHuaheshang = new General("侠客鲁智深");
General *pLida = new General("莽汉李逵");
General *pLinchong = new General("英雄林冲");
General *pGuansheng = new General("大刀关胜");
pHorseArmy->Add(pLinchong);
pHorseArmy->Add(pGuansheng);
pPaceArmy->Add(pWusong);
pPaceArmy->Add(pHuaheshang);
pPaceArmy->Add(pLida);
pArmy.Add(pHorseArmy);
pArmy.Add(pPaceArmy);
pArmy.Assault();
return 0;
}
运行结果如下:
我们可以看到,通过组合模式,对于对象的调用变得简单了起来,只要通过一个函数就可以实现对所有对象的统一调用。这样做最大的好处就是将所有的代码统一成了一份,可以避免在不同的地方出现类似的代码。在多人合作的情况下,由于交流不充分的问题,很多时候开发人员各自写各自的代码,而不关心别人的代码,极容易产生相同功能的代码。当我们的类进行扩展的时候,可能很多地方都需要进行修改。
我们再来看看组合模式可能存在的问题。Leaf类继承了Component中的所有方法,但对于Add,Remove,GetChild等操作,这些操作对它来说是无意义的。我们也可以把这些操作放到Composite中,但在这种情况下,我们将无法通过基类来直接操作Composite对象。个人觉得这个问题可以根据具体问题权衡解决。我们也可以为Component扩展更多的Leaf或者Composite,在这种情况下,如果我们想控制Composite中只能添加限定的Leaf或者Composite在静态情况下是不可能的,必须在运行时刻去判断。
还有,我们应该最大化Component接口,我们使用该接口的目的就是使用户不知道它具体操作的是哪个Leaf或者Composite,所以我们应该尽量多定义一些它们公共的操作。这样还是会造成我们上面提到的会给一些类带来无意义的功能。
我们再来简单比较一下Composite和Decorator模式,这两个模式都会有一个子类拥有基类的指针,不同之处是Composite会拥有多个组件,而Decorator只拥有一个组件。Composite主要目的是对象的聚集,使它们的调用统一化;而Decorator不是此目的,它的目的是给对象添加一些额外的功能。
好了,Composite的讲解先到此为止。如果你感兴趣的话,可以试着把我们第一章用到的策略模式应用到该处。不过我们下面看另外一个问题。在main中,我们需要很多代码去完成对象的创建。我们是否可以将对象的创建部分也做一些封装呢?我们可以找找创建型的模式中是否有适合我们的模式?
在《 设计模式 》中,我们可以找到Builder模式,它的定义是:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。和我们的意图有点象,先来看看《设计模式》中为我们提供的类图:
作一下解释:
1, Director是具体要用到复杂对象的部分(如GeneralComponent)的类(我们程序中是main中调用)。它会通过Builder来建造自己需要的对象,而不需要自己通过new来得到所要的对象。
2, 而Builder的是一个抽象的创建对象的接口。
3, ConcreteBuilder是我们用来具体创建对象的类。它可能有多个,用来帮我们建造不同类型的对象。如我们把我们的General派生出不同的风格,如先秦风格和罗马风格的时候,我们可能需要生成不同的类。而每次创建一种风格的对象,我们就可以扩展Builder而生成不同的创建类。
我们的例子比较简单,不需要这么多东西。Builder的重要特征是:它是用来一步一步创建复杂对象类型的。我们的组合类由于需要组合不同的组件,所以整个创建过程比较复杂,那么可以借用Builder模式的特点来创建我们的对象。好的,看看具体的代码:
class Builder
{
public:
static GeneralComponent* CreateCompise(GeneralComponent *pParent,string strArr[],int iLength = 1)
{
if ( pParent != NULL )
{
for( int i = 0 ; i < iLength ; i++ )
{
pParent->Add(new GeneralComposite(strArr[i]));
}
return pParent;
}
else
{
return new GeneralComposite(strArr[0]);
}
}
static GeneralComponent* CreateGeneral(GeneralComponent *pParent,string strArr[],int iLength = 1)
{
if ( pParent != NULL )
{
for( int i = 0 ; i < iLength ; i++ )
{
pParent->Add(new General(strArr[i]));
}
return pParent;
}
else
{
return new General(strArr[0]);
}
}
};
这边的代码略显简单,只重意了,你可以看到,创建过程被封装成CreateCompise和CreateGeneral两个方法,通过它们来创建我们所需要的对象。只要将所需要的Composite(为它创建Leaf和子Composite的),还有创建对象所需要的参数(例子有些简单,只要一个初始化参数)数组和数组长度传进函数就可以帮我们创建所需要的对象了。由于Builder没有成员变量,将两个函数设置成了静态函数,想当于一个单件类型。我们再看看它的具体用法:
int main(int argc, char* argv[])
{
string strArmy[1] = {"梁山大军"};
string strArmyChild[2] = {"梁山马军","梁山步军"};
string strSpecLeader[3] = {"好汉武松","侠客鲁智深","莽汉李逵"};
string strHorseLeader[2] = {"英雄林冲","大刀关胜"};
GeneralComponent *pArmy = Builder::CreateCompise(NULL,strArmy);
Builder::CreateCompise(pArmy,strArmyChild,2);
Builder::CreateGeneral(pArmy->GetChild(0),strHorseLeader,2);
Builder::CreateGeneral(pArmy->GetChild(1),strSpecLeader,3);
pArmy->Assault();
delete pArmy;
}
这样我们就可以统一对象的创建过程。做到一定程度的聚合。我个人感觉模式的学习过程是从形到意(就是从一些具体的例子看起,通过具体的例子来理解模式);而后是重意不重形(明白模式的意义之后,我们就可以随意使用模式了,而不需要硬套公式)。所以此处使用了和书上并不一致的Builder模型。其实Builder模式与Abstract Factory模式很相似,我们下次描述Abstract Factory模式的时候,再回头比较一下。