意图
将对象组合成树形结构以表示 “部分-整体” 的层次结构。组成模式使得用户对单个对象和组合对象的使用具有一致性。
动机
一个很明显的例子,文件系统, 如下图:
我客户端在展示这样的层级结构的时候,很明显需要将这样的层级结构先抽象成2部分,一部分是文件(各种类型文件),另一部分为文件夹,它下面是可以有各种类型的文件,并且可以自由添加层次
这时候可能会有人说没有必要,毕竟只是展示,全都是 Item
就完事,确实,但是这样是很不利于扩展的,简单的需求变化,可能要改的就很多,比如:
- 文件夹需要使用文件夹的图标,视频需要使用视频的图标,不同类型的文件使用不同的展示图标。文件夹(
枝节点
可以有子节点的节点) 是一类的, 文件(叶节点
没有子节点的节点) 各有各的变化,但可以抽象成一类 - 如果在展示的层级的列表中我需要添加一些鼠标事件,双击 文件夹 展开显示它的直接下级,这里所有的文件夹操作都是相同的, 双击 文件 使用对应的显示工具预览,比如视频文件使用播放器播放,文档文件使用记事本打开,所以在核心上,这二者的行为是不一致,需要独立设计
上面一部分主要解释,树形的结构中 叶节点
和 枝节点
需要分开设计的原因, 所有的 枝节点
都具有相同的特性,而 叶节点
功能和 枝节点
出入很大,需要单独抽象, 并且 枝节点
需要管理其下的所有 子节点
,例如图片中,视频文件属于视频文件夹,又比如 《大话设计模式》 中提到的部门和地区的关系,公司总部下有财务部门,人事部门,北京分部的地方部门也有财务部门,人事部门, 这里的树结构中,公司总部,北京分部就相当于 枝节点
, 财务部门和人事部分就相当于 叶节点
说了很多,没有说明单个对象和组合对象的使用具有一致性这一点,还是以图片中文件系统为例
我删除一个文件和我删除一个文件夹,或者将一个文件移到到另一个地方和移动一个文件夹到另一个地方,我在使用过程中必然希望代码是一致的,不可能根据对象的不同来区分操作,不管是一个文件,还是一个文件夹下的所有文件,他们就是一个 部分-整体
的聚合关系,再对对象的处理上应该是一致的
所以就需要将 叶节点
和 枝节点
在进一部分抽象
使用场景
- 表示对象的部分-整体层次结构
- 希望用户忽略组合对象与单个对象的不同,用户将统一的使用组合结果中的所有对象
UML 图
Component
: 1. 为组合中的对象声明接口 2. 在适当情况下,实现所有类共有接口的缺省行为 3. 声明一个接口用于访问和管理Component
的子部件 4.(可选)提供一个访问父部件的接口Leaf
: 在组合中表示叶节点对象,叶节点没有子节点Composite
: 1. 定义有子部件的那些部件的行为 2. 存储子部件 3. 在Component
接口中实现与子部件有关的操作,比如增加Add
和删除Remove
Client
: 通过Component
接口操作组合部件的对象
这里的 UML 图分为了 透明方式 和 安全方式,因为在组合模式中,节点实际上被分为了 叶节点
和 枝节点
,其中 叶节点
是没有子节点,这就意味着它并没有与子部件有关的操作,比如 Add
和 Remove
,而 Leaf(叶节点)
和 Composite(枝节点)
都继承自 Component
透明方式 :Component
中声明所有用来管理子对象的方法,包括 Add
和 Remove
等,那么叶节点和枝节点对于外界没有区别,它们具备 完全一致 的行为接口。但是问题很明显, Leaf
实际上是不具备 Add()
、Remove()
方法,所有实现的没有意义
安全方式 :Component
中不去声明管理子对象的方法,那么子类的 Leaf
也就不需要去实现它,而是在 Composite
声明所有用来管理子类对象的方法,但是这样由于不够透明,所有树叶和树枝类将不再具有相同的接口,客户端的调用需要做相应的判断,带来了不便。
为了避免 Leaf
执行 Add()
和 Remove()
操作, 同时又不希望通过 类型转换 来区分是否是 Leaf
还是 Composite
, 可以在基类中 Component
中提供一个返回类型的虚函数,例如默认返回 true
表示 Leaf
, Composite
重新实现,返回 false
而 GOF
中提到的是在 Component
中声明一个操作 Composite* GetComposite()
, Component
提供一个返回空指针的缺省操作。 Composite
类重新定义这个操作并通过 this
指针返回它自身
举个例子
就是用文件系统的例子,首先是 Component
类,定义了 Add()
, Remove()
, Show()
以及 Open()
#ifndef COMPONENT_H
#define COMPONENT_H
#include <iostream>
#define UNUSED(x) (void)x;
class Component
{
public:
Component(std::string title) : m_title(title) {}
virtual ~Component(){}
virtual Component *getComposite() { return nullptr; }
void Add(Component* c)
{
if (auto comp = getComposite())
comp->realAdd(c);
}
void Remove(Component* c)
{
if (auto comp = getComposite())
comp->realRemove(c);
}
virtual void Show(int iDepth = 1) = 0;
virtual void Open() = 0;
private:
virtual void realAdd(Component* c) { UNUSED(c) }
virtual void realRemove(Component* c) { UNUSED(c) }
protected:
std::string m_title;
};
#endif // COMPONENT_H
接下来就是文件夹的实现, 这里析构需要将它管理的 Component
删除
#ifndef COMPOSITE_H
#define COMPOSITE_H
#include "component.h"
#include <list>
#include <iostream>
class Folder : public Component
{
public:
Folder(std::string title) :Component(title) {}
~Folder()
{
std::cout << __func__ << " : "<< m_title << std::endl;
while (!m_listComponents.empty()) {
delete m_listComponents.front(),m_listComponents.pop_front();
}
}
Component *getComposite() { return this; }
virtual void Show(int iDepth)
{
std::cout << std::string(iDepth, '-') << m_title << std::endl;
for (auto comp : m_listComponents) {
comp->Show(iDepth+1);
}
}
virtual void Open()
{
std::cout << "\""<< m_title << "\"文件夹下文件为:"<< std::endl;
for (auto comp : m_listComponents) {
comp->Show(0);
}
}
private:
virtual void realAdd(Component* c) { m_listComponents.push_back(c); }
virtual void realRemove(Component* c) { m_listComponents.remove(c);}
private:
std::list<Component *> m_listComponents;
};
#endif // COMPOSITE_H
文件这一块比较简单,但是为了之后的扩展,顺便实现了 VideoFile
, DocmFile
, ImageFile
它们的打开方式自己实现
#ifndef LEAF_H
#define LEAF_H
#include "component.h"
#include <iostream>
class File : public Component
{
public:
File(std::string title) : Component(title) {}
~File() {}
virtual void Show(int iDepth) { std::cout << string(iDepth,'-') << m_title << std::endl; }
virtual void Open() = 0;
};
class VideoFile : public File
{
public:
VideoFile(std::string title) : File(title) {}
~VideoFile() { std::cout << __func__ << std::endl; }
void Open() { std::cout << "视频文件使用 vlc 打开" << std::endl; }
};
class DocmFile : public File
{
public:
DocmFile(std::string title) : File(title) {}
~DocmFile() { std::cout << __func__ << std::endl; }
void Open() { std::cout << "文本文件使用 记事本 打开" << std::endl; }
};
class ImageFile : public File
{
public:
ImageFile(std::string title) : File(title) {}
~ImageFile() { std::cout << __func__ << std::endl; }
void Open() { std::cout << "图片文件使用 相片 打开" << std::endl; }
};
#endif // LEAF_H
为了方便对文件的创建,所有使用一个简单工厂对文件进行了封装
#ifndef FILEFACTORY_H
#define FILEFACTORY_H
#include "leaf.h"
class FileFactory
{
public:
static FileFactory& getInstance()
{
static FileFactory value;
return value;
}
File * getFile(std::string type, std::string title)
{
if ( type == "video") {
return new VideoFile(title);
} else if ( type == "docm") {
return new DocmFile(title);
} else if ( type == "image") {
return new ImageFile(title);
} else {
return nullptr;
}
}
private:
FileFactory() = default;
FileFactory(const FileFactory& other)=delete;
FileFactory& operator=(const FileFactory&)=delete;
};
#endif // FILEFACTORY_H
最后便是客户端的调用了
#include <iostream>
#include "component.h"
#include "composite.h"
#include "filefactory.h"
#define SAFE_DELETE(x) {if(x){ delete x; x= nullptr;}}
int main()
{
Component *root_folder = new Folder("Demo");
Component *video_folder = new Folder("视频");
Component *video =FileFactory::getInstance().getFile("video", "xiamu.mp4");
video_folder->Add(video);
Component *image_folder = new Folder("图片");
Component *image = FileFactory::getInstance().getFile("image", "function.png");
image_folder->Add(image);
Component *docm_folder = new Folder("文档");
Component *docm = FileFactory::getInstance().getFile("docm", "组成模式.md");
docm_folder->Add(docm);
root_folder->Add(video_folder);
root_folder->Add(image_folder);
root_folder->Add(docm_folder);
std::cout << "展示树结构:"<< std::endl;
root_folder->Show();
std::cout << string(20, '-')<< std::endl;
std::cout << "依次打开文件:"<< std::endl;
video->Open();
image->Open();
docm->Open();
std::cout << string(20, '-')<< std::endl;
std::cout << "展示文件夹下的文件名称:"<< std::endl;
video_folder->Open();
image_folder->Open();
docm_folder->Open();
std::cout << string(20, '-')<< std::endl;
std::cout << "析构:"<< std::endl;
SAFE_DELETE(root_folder);
return 0;
}
运行结果:
展示树结构:
-Demo
--视频
---xiamu.mp4
--图片
---function.png
--文档
---组成模式.md
--------------------
依次打开文件:
视频文件使用 vlc 打开
图片文件使用 相片 打开
文本文件使用 记事本 打开
--------------------
展示文件夹下的文件名称:
"视频"文件夹下文件为:
xiamu.mp4
"图片"文件夹下文件为:
function.png
"文档"文件夹下文件为:
组成模式.md
--------------------
析构:
~Folder : Demo
~Folder : 视频
~VideoFile
~Folder : 图片
~ImageFile
~Folder : 文档
~DocmFile
补充
而关于存储子部件部分,其实还有几点值得说道的
- 存储子部件应该使用怎样的数据结构?
效率优先 - 枝叶节点保存的
Composite
指针应该由谁删除的问题?
当一个Composite
被销毁的时候,通常最好由Composite
负责删除其子节点 - 树的深度过深且频繁遍历和查找,如何保证速度?
Composite
可以缓冲存储对它的子节点进行遍历或查找的相关信息。注意一点:当底层一个组件发生改变,原先的缓冲存储的信息也就是失效了,所以这时候需要定义一个接口来通知父部件
关于 组成模式
的总结大概就这些了~