组成模式

意图

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

动机

一个很明显的例子,文件系统, 如下图:

uml

我客户端在展示这样的层级结构的时候,很明显需要将这样的层级结构先抽象成2部分,一部分是文件(各种类型文件),另一部分为文件夹,它下面是可以有各种类型的文件,并且可以自由添加层次

这时候可能会有人说没有必要,毕竟只是展示,全都是 Item 就完事,确实,但是这样是很不利于扩展的,简单的需求变化,可能要改的就很多,比如:

  1. 文件夹需要使用文件夹的图标,视频需要使用视频的图标,不同类型的文件使用不同的展示图标。文件夹(枝节点可以有子节点的节点) 是一类的, 文件(叶节点没有子节点的节点) 各有各的变化,但可以抽象成一类
  2. 如果在展示的层级的列表中我需要添加一些鼠标事件,双击 文件夹 展开显示它的直接下级,这里所有的文件夹操作都是相同的, 双击 文件 使用对应的显示工具预览,比如视频文件使用播放器播放,文档文件使用记事本打开,所以在核心上,这二者的行为是不一致,需要独立设计

上面一部分主要解释,树形的结构中 叶节点枝节点 需要分开设计的原因, 所有的 枝节点 都具有相同的特性,而 叶节点 功能和 枝节点 出入很大,需要单独抽象, 并且 枝节点 需要管理其下的所有 子节点,例如图片中,视频文件属于视频文件夹,又比如 《大话设计模式》 中提到的部门和地区的关系,公司总部下有财务部门,人事部门,北京分部的地方部门也有财务部门,人事部门, 这里的树结构中,公司总部,北京分部就相当于 枝节点, 财务部门和人事部分就相当于 叶节点

说了很多,没有说明单个对象和组合对象的使用具有一致性这一点,还是以图片中文件系统为例

我删除一个文件和我删除一个文件夹,或者将一个文件移到到另一个地方和移动一个文件夹到另一个地方,我在使用过程中必然希望代码是一致的,不可能根据对象的不同来区分操作,不管是一个文件,还是一个文件夹下的所有文件,他们就是一个 部分-整体 的聚合关系,再对对象的处理上应该是一致的

所以就需要将 叶节点枝节点 在进一部分抽象

使用场景

  • 表示对象的部分-整体层次结构
  • 希望用户忽略组合对象与单个对象的不同,用户将统一的使用组合结果中的所有对象

UML 图

uml

  • Component : 1. 为组合中的对象声明接口 2. 在适当情况下,实现所有类共有接口的缺省行为 3. 声明一个接口用于访问和管理 Component 的子部件 4.(可选)提供一个访问父部件的接口
  • Leaf : 在组合中表示叶节点对象,叶节点没有子节点
  • Composite : 1. 定义有子部件的那些部件的行为 2. 存储子部件 3. 在 Component 接口中实现与子部件有关的操作,比如增加 Add 和删除 Remove
  • Client : 通过 Component 接口操作组合部件的对象

这里的 UML 图分为了 透明方式安全方式,因为在组合模式中,节点实际上被分为了 叶节点枝节点,其中 叶节点 是没有子节点,这就意味着它并没有与子部件有关的操作,比如 AddRemove,而 Leaf(叶节点)Composite(枝节点) 都继承自 Component

透明方式Component 中声明所有用来管理子对象的方法,包括 AddRemove 等,那么叶节点和枝节点对于外界没有区别,它们具备 完全一致 的行为接口。但是问题很明显, Leaf 实际上是不具备 Add()Remove() 方法,所有实现的没有意义

安全方式Component 中不去声明管理子对象的方法,那么子类的 Leaf 也就不需要去实现它,而是在 Composite 声明所有用来管理子类对象的方法,但是这样由于不够透明,所有树叶和树枝类将不再具有相同的接口,客户端的调用需要做相应的判断,带来了不便。

为了避免 Leaf 执行 Add()Remove() 操作, 同时又不希望通过 类型转换 来区分是否是 Leaf 还是 Composite, 可以在基类中 Component 中提供一个返回类型的虚函数,例如默认返回 true 表示 LeafComposite 重新实现,返回 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 可以缓冲存储对它的子节点进行遍历或查找的相关信息。注意一点:当底层一个组件发生改变,原先的缓冲存储的信息也就是失效了,所以这时候需要定义一个接口来通知父部件

关于 组成模式 的总结大概就这些了~