现在的位置: 首页 > 综合 > 正文

设计模式 – 组合模式

2013年12月03日 ⁄ 综合 ⁄ 共 3881字 ⁄ 字号 评论关闭

讲完了适配器和桥接模式,我们这次来看看组合模式(Composite)。

 

意图

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

 

结构图

从上面的结构图可以看出,Composite聚合了Component,那么也就是说Composite里面可以保存任何Component的子类,比如一个Leaf或者另外一个Composite,然后另外一个Composite里面又可以有Leaf和Composite。这个其实也就是一个递归结构。来举几个例子吧:

1. 目录和文件,目录里面可以有文件还可以有子目录,子目录又可以有文件和子目录。这样我们可以给文件和目录抽象出一个统一的接口比如叫做Storage,然后文件和目录是Storage的子类。那么我们可以把文件和目录同样对待,不需要区分。Storage相当于上图中的Component,文件相当于Leaf,目录相当于Composite。

2. 树叶和树枝。树枝里面可以有树叶和子树枝,子树枝又可以有子树枝和树叶。那么我们也应该把树叶和树枝同等对待。给它们一个统一接口,比如叫做TreeComponent,那么TreeComponent就是上图中的Component,树叶就是Leaf,树枝就是Composite。

3. 再来一个例子,图片和图元,比如一个图片里面有直线和正方形,这个图片本身又可以作为另外一个图片的一部分。其中直线和正方形就是Leaf,图片就是Composite,给它们一个统一接口Graphic,那么Graphic就是Component。

下面我们就以图片为例,来介绍一下组合模式。先来看看对象结构图:

 从上面的图中可以很清楚的看到,picture可以有text,line,rect组成,而一个picture又可以作为另外一个picture的一部分。这个就是个递归的关系。如果我们可以把picture和line,text,rect同等对待的话,我们就可以很方便的组合一个更大的对象。

先给出类图:

这个例子代码里面总共有5个类:

CGraphic:这是个基类,抽象了Add,Draw和Remove函数,对应于模式结构图里面的Component;

CLine/CText/CRect:这3个类分别画线,文字和正方形,对应于模式结构图里面的Leaf;

CPicture:图片类,图片类由基本图片单元Line,Text,Rect组成,从上面的类图中就可以看出CPicture聚合了CGraphic。CPicture就是Composite类。

先看看CGraphic代码:

class CGraphic
{
public:
	virtual void Add(CGraphic* g){};
	virtual void Remove(CGraphic* g){};

	virtual void Draw() = 0;
};

CGraphic里面抽象了3个函数Add,Remove和Draw。Add/Remove是用来给Composite(CPicture)添加/删除子部件的。Leaf不需要这2个函数,但是这2个函数也会被CLine/CText/CRect继承,这个怎么办呢?这里先留个疑问,我们后面再探讨这个问题。
给出Leaf类代码:

class CLine: public CGraphic
{
public:
	virtual void Draw()
	{
		std::cout << "Draw line\n";
	}
};

class CText: public CGraphic
{
public:
	virtual void Draw()
	{
		std::cout << "Draw text\n";
	}
};

class CRect: public CGraphic
{
public:
	virtual void Draw()
	{
		std::cout << "Draw rect\n";
	}
};

简单明了,就是实现各自的Draw函数。

看看CPicture(Composite)代码:

class CPicture: public CGraphic
{
public:
	virtual void Add(CGraphic* g)
	{
		if(std::find(_listGraphics.begin(), _listGraphics.end(), g) == _listGraphics.end())
			_listGraphics.push_back(g);
	}

	virtual void Remove(CGraphic* g)
	{
		_listGraphics.remove(g);
	}

	virtual void Draw()
	{
		BOOST_FOREACH(CGraphic* g, _listGraphics)
		{
			g->Draw();
		}
	}

	virtual ~CPicture()
	{
		BOOST_FOREACH(CGraphic* g, _listGraphics)
		{
			delete g;
		}
	}

protected:
	std::list<CGraphic*> _listGraphics;
};

我们可以看到CPicture有一个数据成员std::list<CGraphic*> _listGraphics。_listGraphics保存了CPicture的子部件,这个就是用来实现递归的关键,_listGraphics里面可以保存CGraphic的任何子类:Leaf和Composite。也就是说我们把Leaf和Composite同等对待了,他们统统可以放到_listGraphics里面。
 看看客户端如何调用:

CGraphic* line = new CLine();
	CGraphic* text = new CText();
	CGraphic* rect = new CRect();

	//生成一个图片对象,这个图片对象里面包括一条直线和一段文字
	CGraphic* pic1 = new CPicture();
	pic1->Add(line);
	pic1->Add(text);

	//生成另外一个图片对象,这个图片对象里面包括2部分:一个图片pic1和一个正方形。换言之,这个图片里面有一条直线,一段文字和一个正方形。
	CGraphic* pic2 = new CPicture();
	pic2->Add(pic1);
	pic2->Add(rect);

	//pic2的Draw函数可以画出一条直线,一段文字和一个正方形。
	pic2->Draw();

	delete pic2;

这里生成了一个图片pic2,pic2由一个图片子部件pic1和一个子部件正方形组成。pic1里面又有一条直线和一段文字。我们可以将pic2再当作另外一个图片的子部件。

通过这个例子,我们可以看到,通过组合模式(Composite)是很容易来生成一个大的对象的。

我们来看看组合模式有哪些优点:

1. 定义了包含基本对象和组合对象的类层次结构。基本对象可以被组合对象组合成更复杂的组合对象,而这个组合对象又可以被组合,这样不断的递归下去。客户代码中,任何用到基本对象的地方都可以使用组合对象。

 2. 简化客户代码。客户可以一致地使用基本对象和组合对象。通常用户不知道(也不关心)处理的是一个Leaf还是一个Composite。这就简化了客户代码,因为在定义组合的那些类中不需要来判断子部件是Leaf还是Composite。就像上面的例子中,CPicture用std::list<CGraphic*> _listGraphics 来保存子部件。CPicture并不需要关心子部件是CLine还是CPicture。

3. 使得更容易增加新类型的组件。新定义的Leaf和Composite可以自动地和旧的代码一起工作,客户代码不需要为新的Composite类更改代码。

 

缺点:

很难限制组合中的组件。有时可能希望一个组合只能有某些特定的组件。使用Composite的时候,我们无法静态的来做这些限制,因为所有的Composite和Leaf都继承于同一个接口,没有什么特征。当然我们可以尝试在运行的时候来进行检查。比如给CGraphic增加一个string属性,CLine设置为“line",CPicture设置为“picture”,那么我们可以在运行的时候来读取这个string,从而可以做一些判断。

 

 回想文章的前面部分,我们留了一个疑问,就是CGraphic里面有2个函数Add和Remove,这2个函数CPicture需要使用,但是CLine/CText/CRect都不需要使用。那么我们有必要把这个2个函数放在CGraphic里面吗?我们可以放到CPicture里面吗?对于这个问题,我们需要在安全性和透明性之间做出权衡选择。

1. 在基类里面定义Add和Remove具有很好的透明性。因为客户代码可以一致地使用Leaf和Composite。但是这里有一个安全问题,有时客户会做一些无意义的事情,比如在Leaf中调用Add和Remove。

2. 将基类中的Add和Remove移到Composite中,这样安全性就好一些了。像C++这种静态类型语言,在Leaf中是无法调用Add和Remove的。但是这又损失了透明性,因为Leaf和Composite具有不同的接口。

Composite模式中,我们更加倾向于透明性,因为我们想将Leaf和Composite同等对待。如果真的因为基类封装了Add和Remove而导致一些严重的安全问题,那么我们可以考虑移到Composite中。其实,我个人觉得这个问题正是使用Composite模式的一个难点,有时很难在透明性和安全性之间做出选择,因为各有各的优点缺点。总之,视情况而定吧。

 

抱歉!评论已关闭.