码迷,mamicode.com
首页 > 其他好文 > 详细

设计模式学习笔记--组合模式

时间:2016-05-13 03:27:25      阅读:215      评论:0      收藏:0      [点我收藏+]

标签:

一.简介


今天来学习一下组合模式。说到组合模式,可能比较陌生,但是有很多地方是非常适合使用组合模式的。比如我们电脑系统内的文件系统。文件包含在文件夹内,而文件夹有可能还被其他文件夹包含,整个文件系统呈一个树形结构。对于文件的操作,我们可以操作整个文件夹,相应的文件夹下所有的子文件,子文件夹都会被递归的操作。而我们并不需要知道文件夹下面是什么东东,只需要对根文件夹操作即可。
下面看一下组合模式的定义以及UML类图:
组合模式(Composite Pattern):组合多个对象形成树形结构以表示具有“整体—部分”关系的层次结构。组合模式对单个对象(即叶子对象)和组合对象(即容器对象)的使用具有一致性,组合模式又可以称为“整体—部分”(Part-Whole)模式,它是一种对象结构型模式。
技术分享

二.组合模式的例子



好了,要想更加深刻地理解设计模式,还是要通过实际的例子才能获得更深刻的印象。就拿我们最常用的文件系统来说,如果这个文件系统需要我们来实现,那我们需要怎么实现文件系统的结构呢?给文件实现一个最简单的功能。我们知道,文件系统中分为文件夹和具体的文件,而文件夹内可能包含文件,也可能包含文件夹,整体呈现树形结构,下面我们使用代码来实现以下文件系统。

1.没有使用组合模式的情况

// Design Pattern.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <iostream>
#include <string>
#include <vector>
using namespace std;

//文件类
class TextFile
{
private:
	string name;
public:
	TextFile(string n)
		: name(n)
	{
	
	}

	//文件操作功能
	void Option()
	{
		cout << name <<" 文件操作" << endl;
	}
};

class FileFolder
{
private:
	string name;
	vector<FileFolder*> fileFolderVec;
	vector<TextFile*> textFileVec;
public:
	FileFolder(string n)
		: name(n)
	{

	}

	void AddFile(TextFile* file)
	{
		textFileVec.push_back(file);
	}

	void AddFileFolder(FileFolder* folder)
	{
		fileFolderVec.push_back(folder);
	}

	void Option()
	{
		cout<< name << " 文件夹被操作" << endl;
		//先遍历文件容器,将文件夹本身包含的文件进行操作
		for (vector<TextFile*>::iterator it = textFileVec.begin(); it != textFileVec.end(); ++it)
		{
			(*it)->Option();
		}
		//再遍历文件夹容器,递归调用文件夹下文件夹的操作
		for (vector<FileFolder*>::iterator it = fileFolderVec.begin(); it != fileFolderVec.end(); ++it)
		{
			(*it)->Option();
		}
	}

};


int _tmain(int argc, _TCHAR* argv[])
{
	//我们创建几个文件fileA1,fileA2, fileB1, fileB2, fileC1和文件夹folderA,folderB,folderC
	TextFile* fileA1 = new TextFile("fileA1");
	TextFile* fileA2 = new TextFile("fileA2");
	TextFile* fileB1 = new TextFile("fileB1");
	TextFile* fileB2 = new TextFile("fileB2");
	TextFile* fileC1 = new TextFile("fileC1");

	FileFolder* folderA = new FileFolder("folderA");
	FileFolder* folderB = new FileFolder("folderB");
	FileFolder* folderC = new FileFolder("folderC");

	//设置文件夹与文件之间的包含关系为C(A(A1,A2) B(B1,B2),C1)
	folderA->AddFile(fileA1);
	folderA->AddFile(fileA2);
	folderB->AddFile(fileB1);
	folderB->AddFile(fileB2);
	folderC->AddFile(fileC1);
	folderC->AddFileFolder(folderA);
	folderC->AddFileFolder(folderB);

	//操作C文件夹,递归操作C文件夹下所有文件夹与文件
	folderC->Option();

	system("pause");
	

	return 0;
}
结果:
folderC 文件夹被操作
fileC1 文件操作
folderA 文件夹被操作
fileA1 文件操作
fileA2 文件操作
folderB 文件夹被操作
fileB1 文件操作
fileB2 文件操作
请按任意键继续. . .

2.使用了组合模式的情况

上面的代码虽然成功地实现了我们想要的功能,但是,我们仔细观察一下,文件夹和文件是都可以接受Option操作的,这一部分如果能够将这个option操作抽象到一个基类里面就可以大大优化代码,而且在上面的例子里,我们对于filefolder和textfile是分开存储和添加的,这里也是值得抽象的地方,所以我们很容易地就可以想到,我们可以将textfile和filefolder看做同类型的东东来处理。下面看一下使用组合模式重构之后的代码:
// Design Pattern.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <iostream>
#include <string>
#include <vector>
using namespace std;

//抽象出来一个文件夹和文件的基类
class Component
{
public:
	virtual void Add(Component* component) = 0;
	virtual void Option() = 0;
};

//文件类
class TextFile : public Component
{
private:
	string name;
public:
	TextFile(string n)
		: name(n)
	{

	}

	//覆写添加子节点的功能,由于叶子节点没有增加子节点的功能,这里给出错误提示
	virtual void Add(Component* component) override
	{
		cout << "非法操作,文件下不可再包含文件!" << endl;
	}

	//覆写操作功能
	virtual void Option() override
	{
		cout << name <<" 文件操作" << endl;
	}
};

//文件夹类
class FileFolder : public Component
{
private:
	string name;
	vector<Component*> componentVec;
public:
	FileFolder(string n)
		: name(n)
	{

	}

	//覆写添加子节点的功能,针对接口编程,我们可以省略很多重复代码
	void Add(Component* component) override
	{
		componentVec.push_back(component);
	}

	//覆写操作功能
	void Option() override
	{
		cout<< name << " 文件夹被操作" << endl;
		//直接针对所有子节点进行操作
		for (vector<Component*>::iterator it = componentVec.begin(); it != componentVec.end(); ++it)
		{
			(*it)->Option();
		}
	}

};


int _tmain(int argc, _TCHAR* argv[])
{
	//我们创建几个文件fileA1,fileA2, fileB1, fileB2, fileC1和文件夹folderA,folderB,folderC
	TextFile* fileA1 = new TextFile("fileA1");
	TextFile* fileA2 = new TextFile("fileA2");
	TextFile* fileB1 = new TextFile("fileB1");
	TextFile* fileB2 = new TextFile("fileB2");
	TextFile* fileC1 = new TextFile("fileC1");

	FileFolder* folderA = new FileFolder("folderA");
	FileFolder* folderB = new FileFolder("folderB");
	FileFolder* folderC = new FileFolder("folderC");

	//设置文件夹与文件之间的包含关系为C(A(A1,A2) B(B1,B2),C1),所有的操作都是用Add来进行
	folderA->Add(fileA1);
	folderA->Add(fileA2);
	folderB->Add(fileB1);
	folderB->Add(fileB2);
	folderC->Add(fileC1);
	folderC->Add(folderA);
	folderC->Add(folderB);

	//操作C文件夹,递归操作C文件夹下所有文件夹与文件
	folderC->Option();

	system("pause");
	

	return 0;
}
结果:
folderC 文件夹被操作
fileC1 文件操作
folderA 文件夹被操作
fileA1 文件操作
fileA2 文件操作
folderB 文件夹被操作
fileB1 文件操作
fileB2 文件操作
请按任意键继续. . .

通过组合模式,我们去除了冗余的代码,所有操作都是基于接口Component类进行的,针对接口编程,可以大大减少冗余,也方便今后扩展,比如我们以后可能增加ImageFile类,而我们之前的代码不用进行改动,符合开放-封闭原则。

三.透明方式与安全方式


组合模式有两种体现形式,分别是透明方式和安全方式。两者各有优缺点,下面我们分别来看一下两种方式。

1.透明方式


所谓“透明”,在计算机行业貌似大多指的就是我们不用考虑其中的细节,换句话说我们知道怎么用就好,不需要考虑内部实现等。而组合模式的透明方式在这里指的就是我们可以将容器与叶子节点看成相同的东东,换句话说,增加子节点,移除子节点等操作都在基类中声明出来了。不管是是叶子节点还是容器节点都包含了增加删除子节点的操作,容器节点没有问题,但是叶子节点包含再增加子节点的方法就不正确了,所以如果客户端错误地调用了这个方法,肯定是会造成一定麻烦的,我们在使用透明方式时,一般都是将叶子节点的增加删除子节点方法覆写成为错误提示或者异常处理的方法。
具体的例子如上一节所写,我们上面的例子就是透明方式的组合模式实现,这里不再赘述。

2.安全方式


上面所说,透明方式可能叶子节点继续调用本不该存在的方法的安全隐患,所以这里给出了一种更加安全的方式来实现组合模式。为了不错误调用不该使用的方法,最好的办法也是最笨的办法就是不给出这个方法,所以在安全模式下,对于增加删除节点的操作并没有放在基类中,而是放在了容器类中单独实现,所以叶子节点类中没有增删的方法,也就排除了非法方法被调用的安全隐患,故称为安全方式。
把之前的透明方式改成安全方式代码如下,其实就是讲Add方法从Component类中移到FileFolder类中。
// Design Pattern.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <iostream>
#include <string>
#include <vector>
using namespace std;

//抽象出来一个文件夹和文件的基类
class Component
{
public:
	virtual void Option() = 0;
};

//文件类
class TextFile : public Component
{
private:
	string name;
public:
	TextFile(string n)
		: name(n)
	{

	}

	//覆写操作功能
	virtual void Option() override
	{
		cout << name <<" 文件操作" << endl;
	}
};

//文件夹类
class FileFolder : public Component
{
private:
	string name;
	vector<Component*> componentVec;
public:
	FileFolder(string n)
		: name(n)
	{

	}

	//添加子节点的功能
	void Add(Component* component) 
	{
		componentVec.push_back(component);
	}

	//覆写操作功能
	void Option() override
	{
		cout<< name << " 文件夹被操作" << endl;
		//直接针对所有子节点进行操作
		for (vector<Component*>::iterator it = componentVec.begin(); it != componentVec.end(); ++it)
		{
			(*it)->Option();
		}
	}

};


int _tmain(int argc, _TCHAR* argv[])
{
	//我们创建几个文件fileA1,fileA2, fileB1, fileB2, fileC1和文件夹folderA,folderB,folderC
	TextFile* fileA1 = new TextFile("fileA1");
	TextFile* fileA2 = new TextFile("fileA2");
	TextFile* fileB1 = new TextFile("fileB1");
	TextFile* fileB2 = new TextFile("fileB2");
	TextFile* fileC1 = new TextFile("fileC1");

	FileFolder* folderA = new FileFolder("folderA");
	FileFolder* folderB = new FileFolder("folderB");
	FileFolder* folderC = new FileFolder("folderC");

	//设置文件夹与文件之间的包含关系为C(A(A1,A2) B(B1,B2),C1),所有的操作都是用Add来进行
	folderA->Add(fileA1);
	folderA->Add(fileA2);
	folderB->Add(fileB1);
	folderB->Add(fileB2);
	folderC->Add(fileC1);
	folderC->Add(folderA);
	folderC->Add(folderB);

	//操作C文件夹,递归操作C文件夹下所有文件夹与文件
	folderC->Option();

	system("pause");
	

	return 0;
}
结果同上。

虽然安全方式排除了安全隐患,但是凡事都有两面性,安全的代价是不够抽象,这些方法只有在容器类中才有,客户端在使用的时候只能针对叶子节点和容器类分别对待,不能够针对抽象编程。

两种方式各有优缺点,所以在实际应用中都有很多应用,我们在使用的时候可以视情况而定。


四.组合模式的总结


组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制。
      (2) 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码。
      (3) 在组合模式中增加新的容器构件和叶子构件都很方便,无须对现有类库进行任何修改,符合“开闭原则”。
      (4) 组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子对象和容器对象的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单。
最后,来总结一下组合模式的优缺点以及使用时机。
优点:
1)组合模式提供了一种树形结构的很好的解决方案,方便我们进行对象以及子对象的递归操作。
2)我们进行操作时,不需要考虑是叶子节点对象还是容器对象,采用一致的接口进行操作,客户端使用简单。
3)增加新叶子节点或者容器节点都很方便,无需修改原有代码,符合开放-封闭原则。

缺点:
 在增加新构件时很难对容器中的构件类型进行限制。使用组合模式时,很难对容器内部再做一些具体的限制,因为组合模式是根据抽象来进行操作的。

使用时机:
在我们想要构建的系统是树形结构,并且我们所需要进行的操作对叶子节点和容器节点都通用的时候,即我们希望通过一致的操作接口对待叶子节点和容器节点时,我们就可以考虑使用组合模式了!

设计模式学习笔记--组合模式

标签:

原文地址:http://blog.csdn.net/puppet_master/article/details/51338802

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!