文章目录
前言
在画类图的时候,类与类之间有组合关系,聚合关系,我本来以为这个组合模式应该是整体与部分的关系,其实设计模式中的组合模式和类图中的组合不是同一个东西。设计模式中的组合模式更像是一个行为统一且存在结构性的关系,例如树形结构。
1.引例
先看下面一个例子,在资源管理器中的文件目录中有这样的一个文件结构,root下面有common.mk,config.mk,makefile三个文件,以及app,signal,_include三个子文件夹,且子文件夹下还有对应的文件。
左侧是结构图,右侧是树形图。
现在我们用一段代码来实现
#include <iostream>
#include <string>
#include <list>
class File
{
public:
File(std::string name) :m_sname(name) {}
//显示文件名
void ShowName(std::string lvlstr)
{
std::cout<<lvlstr << "-" << m_sname << std::endl;
}
private:
std::string m_sname; //文件名
};
class Dir
{
public:
Dir(std::string name) :m_sname(name) {};
void AddFile(File* pfile)
{
m_childFile.push_back(pfile);
}
void AddDir(Dir* pDir)
{
m_childDir.push_back(pDir);
}
void ShowName(std::string lvlstr)
{
//输出本目录名
std::cout << lvlstr << "+" << m_sname << std::endl;
//输出所包含的文件名
lvlstr += " ";
for (auto iter=m_childFile.begin();iter!=m_childFile.end();++iter)
{
(*iter)->ShowName(lvlstr);
}
//输出所包含的目录名
for (auto iter = m_childDir.begin(); iter != m_childDir.end(); ++iter)
{
(*iter)->ShowName(lvlstr);
}
}
private:
std::string m_sname;
std::list<File*>m_childFile;
std::list<Dir*>m_childDir;
};
int main()
{
//(1)创建各种目录,文件对象
Dir* pdir1 = new Dir("root");
//-----
File* pfile1 = new File("common.mk");
File* pfile2 = new File("config.mk");
File* pfile3 = new File("makefile");
//-----
Dir* pdir2 = new Dir("app");
File* pfile4 = new File("nginx.c");
File* pfile5 = new File("ngx_conf.c");
//-----
Dir * pdir3 = new Dir("signal");
File* pfile6 = new File("ngx_signal.c");
//-------
Dir * pdir4 = new Dir(" include");
File* pfile7 = new File("ngx_func.h");
File* pfile8 = new File("ngx_signal.h");
//(2)构造树形目录结构
pdir1->AddFile(pfile1);
pdir1->AddFile(pfile2);
pdir1->AddFile(pfile3);
//-----
pdir1->AddDir(pdir2);
pdir2->AddFile(pfile4);
pdir2->AddFile(pfile5);
pdir1->AddDir(pdir3);
pdir3->AddFile(pfile6);
pdir1->AddDir(pdir4);
pdir4->AddFile(pfile7);
pdir4->AddFile(pfile8);
//(3)输出整个目录结构,只要调用根目录的ShowName方法即可,
//每个目录都有自己的ShowName方法负责自己的文件和目录的显示
pdir1->ShowName("");
//(4)释放资源
delete pfile8;
delete pfile7;
delete pdir4;
//-------
delete pfile6;
delete pdir3;
//-------
delete pfile5;
delete pfile4;
delete pdir2;
//------
delete pfile3;
delete pfile2;
delete pfile1;
delete pdir1;
}
在上述代码中,我们通过ShowName这个函数用来显示当前文件/目录的名称,这种显示名称的行为是否具有一致性?
对于一开始提及的需求,打印出层级结构信息,我们能否将一致性的操作提取出来呢?
如果不提取出来,那就是像上面代码一样,定义了两个类,这两个类之间的成员函数其实没有任何关系,虽然都是叫做ShowName,但分属于不同的类,如果共有的行为再多一点呢?比如输出类型,ShowType这种,其实行为也是一致的。
由此引出我们的组合模式的原理阐述:
组合模式
将一组对象(文件和目录)组织成树形结构以表示“部分-整体”的层次结构(目录中包含文件和子目录)。使得用户对单个对象(文件)和组合对象(日录)的操作/使用/处理(递归遍历并执行ShowName逻辑等)具有一致性。
在这个例子里面,File类型就是部分,Dir类型就是整体的层次结构。
2.一致性抽象处理
对于行为一致的情况,在设计模式中通常用接口进行抽象,我们再引入接口类FileSystem
#include <iostream>
#include <string>
#include <list>
class FileSystem
{
public:
virtual void ShowName(int level) = 0;//显示名字,用level表示显示的层级,用于显示对齐
virtual int Add(FileSystem* pfilesys) = 0;
virtual int Remove(FileSystem* pfilesys) = 0;
virtual ~FileSystem() {}
};
class File:public FileSystem
{
public:
File(std::string name) :m_sname(name) {}
void ShowName(int level) override
{
for (int i = 0; i < level; ++i) { std::cout << " "; };
std::cout << "-" << m_sname << std::endl;
}
int Add(FileSystem* pfilesys)override
{
return -1;
};
int Remove(FileSystem* pfilesys)override
{
return -1;
};
private:
std::string m_sname; //文件名
};
class Dir :public FileSystem
{
public:
Dir(std::string name) :m_sname(name) {};
int Add(FileSystem* pfilesys)override
{
m_child.push_back(pfilesys);
return 0;
}
int Remove(FileSystem* pDir)override
{
m_child.remove(pDir);
return 0;
}
void ShowName(int level)override
{
for (int i = 0; i < level; ++i) { std::cout << " "; };
std::cout << "+" << m_sname << std::endl;
level++;
for (auto iter=m_child.begin();iter!=m_child.end();++iter)
{
(*iter)->ShowName(level);
}
}
private:
std::string m_sname;
std::list<FileSystem*>m_child;
};
int main()
{
//(1)创建各种目录,文件对象
FileSystem* pdir1 = new Dir("root");
//-----
FileSystem* pfile1 = new File("common.mk");
FileSystem* pfile2 = new File("config.mk");
FileSystem* pfile3 = new File("makefile");
//-----
FileSystem* pdir2 = new Dir("app");
FileSystem* pfile4 = new File("nginx.c");
FileSystem* pfile5 = new File("ngx_conf.c");
//-----
FileSystem * pdir3 = new Dir("signal");
FileSystem* pfile6 = new File("ngx_signal.c");
//-------
FileSystem * pdir4 = new Dir(" include");
FileSystem* pfile7 = new File("ngx_func.h");
FileSystem* pfile8 = new File("ngx_signal.h");
//(2)构造树形目录结构
pdir1->Add(pfile1);
pdir1->Add(pfile2);
pdir1->Add(pfile3);
//-----
pdir1->Add(pdir2);
pdir2->Add(pfile4);
pdir2->Add(pfile5);
pdir1->Add(pdir3);
pdir3->Add(pfile6);
pdir1->Add(pdir4);
pdir4->Add(pfile7);
pdir4->Add(pfile8);
//(3)输出整个目录结构,只要调用根目录的ShowName方法即可,
//每个目录都有自己的ShowName方法负责自己的文件和目录的显示
pdir1->ShowName(0);
//(4)释放资源
delete pfile8;
delete pfile7;
delete pdir4;
//-------
delete pfile6;
delete pdir3;
//-------
delete pfile5;
delete pfile4;
delete pdir2;
//------
delete pfile3;
delete pfile2;
delete pfile1;
delete pdir1;
}
在上述定义中,用户是指main主函数中的调用代码,一致性指不用区分树叶还是树枝,两者都继承自FileSystem,都具有相同的接口,可以做相同的调用。可以看到,组合模式的设计思路其实就是用树形结构来组织数据,然后通过在树中递归遍历各个节点并在每个节点上统一地调用某个接口(例如,都调用ShowName成员函数)或进行某些运算。总之,组合模式之所以称为结构型模式,是因为该模式提供了一个结构,可以同时包容单个对象和组合对象。组合模式发挥作用的前提是具体数据必须能以树形结构的方式表示,树中包含了单个对象和组合对象。该模式专注于树形结构中单个对象和组合对象的递归遍历(只有递归遍历才能体现出组合模式的价值),能把相同的操作(FileSystem 定义的接口)应用在单个以及组合对象上,并且可以忽略单个对象和组合对象之间的差别。
3.透明组合模式与安全组合模式
这里我们将一致性的操作提取出来,作为一个接口,这样满足了用户对单个对象和组合对象的使用具有一致性。可是又引入了新的问题,我的文件File类型,其实不支持Add和Remove接口,没必要支持,因为它已经是一棵树中的叶子节点了,不能再增加和删除节点。这种实现方式在组合模式中称之为透明组合模式
。
它的优点就是保证所有的组件,都有相同的接口,确保了操作的一致性。但是缺点也很明显,存在部分组件需要实现本来不需要的接口,这就导致了冗余,同时也会造成偶发性的错误调用,比如错误的让File类的对象调用了Add函数,但是返回一个-1,并没有任何用处。
针对这个缺陷,我们引入安全组合模式
通过类图,我们可以看到,我们把共有的一致性操作还是抽象出来了,但是对于只有Dir这种独有的Add、Remove等操作,我们放到Dir自身上去定义与实现。
其实我在看书的时候就在想,既然是操作的一致性,为什么还要把Dir的一些接口比如AddFile抽象成Add,再放到FileSystem里面去,明明File没有Add这个操作。但是看了后面的透明组合模式和安全组合模式,我也不得不说,为了引出两个组合模式的区别吧。
最后贴一些组合模式的常用场景
(1)在公司组织结构中采用组合模式,公司组织架构如图
(2)对某个目录下的所有文件(包括子目录的文件)进行杀毒工作
(3)利用图元进行图形的绘制工作
总结
组合模式说的直白一点,两句话。
用在树形结构,存在部分与整体关系中。抽象出整体与部分的共有操作,用统一的接口实现