本文来记录设计模式之组合设计模式。
组合模式是一种结构型模式,代码中涉及到递归调用。组合模式主要用来处理树形结构,如果代码结构不是树形模式,不适用该模式。以下通过一个示例逐步说明树形结构的使用,然后使用组合模式改造下边的范例,并引入组合模式的定义,最后讨论组合模式的使用场景。
一个基本的目录内容遍历范例
下面的代码是多叉树的前序遍历。
            
            
              cpp
              
              
            
          
          // 文件
class File
{
public:
    File(string name) :m_name(name) {}
    // 显示文件名
    void ShowName(string indent)  // 参数表示缩进
    {
        cout << indent << "-" << m_name << endl;// -表示一个文件,没有子节点
    }
private:
    string m_name;  // 文件名称
};
// 目录相关类: 
// 就是一个文件夹下面,可以有一个或多个文件,也可以有一个或多个目录
class Dir
{
public:
    Dir(string name) :m_dirname(name) {}
public:
    // 向当前目录中添加一个文件
    void addFile(File* file)
    {
        m_listChildFile.push_back(file);
    }
    // 向当前目录中添加一个子目录
    void addDir(Dir* dir)
    {
        m_listChildDir.push_back(dir);
    }
    // 显示当前目录的目录名称,同时显示当前目录下面的所有文件和目录
    void showDir(string indent)
    {
        // 1 输出目录名称
        cout << indent << "#" << m_dirname << endl;
        // 2 输出当前目录下面的所有文件	
        indent += "    ";
        for (File* p : m_listChildFile)
        {
            p->ShowName(indent);
        }
        // 3 显示当前目录下面的目录名称, 多叉树遍历
        for (Dir* dir : m_listChildDir)
        {
            dir->showDir(indent);
        }
    }
private:
    string		 m_dirname;		    // 目录名字
    list<File*>  m_listChildFile;	// 当前目录下面的 文件列表
    list<Dir*>   m_listChildDir;	// 当前目录下面的 目录列表
};
void test()
{
    // 创建三个文件
    Dir* pdir1 = new Dir("E:");
    File* pfile1 = new File("1.txt");
    File* pfile2 = new File("2.txt");
    File* pfile3 = new File("3.txt");
    pdir1->addFile(pfile1);
    pdir1->addFile(pfile2);
    pdir1->addFile(pfile3);
    /*
        E:
           1.txt;
           2.txt;
           3.txt;
    */
    // 创建两个目录
    Dir* pdir2 = new Dir("新建文件夹1");
    File* pfile11 = new File("1.txt");
    File* pfile21 = new File("2.txt");
    File* pfile31 = new File("3.txt");
    pdir2->addFile(pfile11);
    pdir2->addFile(pfile21);
    pdir2->addFile(pfile31);
    /*
        新建文件夹1:
            1.txt;
            2.txt;
            3.txt;
    */
    pdir1->addDir(pdir2);
    pdir1->showDir("");
    /*
        E:
            1.txt;
            2.txt;
            3.txt;
            新建文件夹1:
                1.txt;
                2.txt;
                3.txt;
    */
}
        上边的示例对应的目录树如下:

上面示例的特点:
1 将诸多对象以树形结构来组织;
2 以一个树根为起点,可以遍历到所有该根下的树节点。
3 目录文件的ShowName() 是一个递归调用,不但要显示自身的名字,还要显示其下的文件和目录名字。
上面代码存在的问题:为了区分文件和目录,分别创建了File和Dir两个类,这种区分存在代码冗余,为此,引入了组合模式,下面用组合模式改造上边的代码,不再将File和Dir类单独分开,而是引入了一个新的抽象类,并提供公共的接口,而后让File和Dir分别继承自FileDirAbstractSystem类。
使用组合模式改造目录内容遍历范例
            
            
              cpp
              
              
            
          
              // 改进:现在将pFile 和 pDir抽象为 FileDirAbstractSystem类
    class FileDirAbstractSystem
    {
    public:
        virtual void ShowName(int level) = 0;
        virtual void AddChild(FileDirAbstractSystem* pSystem) = 0;
        virtual void RemoveChild(FileDirAbstractSystem* pSystem) = 0;
        virtual ~FileDirAbstractSystem()
        {
        }
    };
    // 实现文件类
    class File : public FileDirAbstractSystem
    {
    public:
        File(string name) : m_fileName(name) {}
        virtual void ShowName(int level) override
        {
            for (int i = 0; i < level; ++i)
            {
                cout << "   "; 
            }
            cout << "-" << m_fileName << endl;
        }
        virtual void AddChild(FileDirAbstractSystem* pSystem) override
        {
            cout << "文件不能添加子节点" << endl;
            return;
        }
        virtual void RemoveChild(FileDirAbstractSystem* pSystem) override
        {
            cout << "文件没有孩子节点" << endl;
            return;
        } 
    private:
        string m_fileName;
    };
    class Dir : public FileDirAbstractSystem
    {
    public:
        Dir(string name) : m_fileName(name) {}
        virtual void ShowName(int level) override
        {
            for (int i = 0; i < level; ++i)
            {
                cout << "   ";
            }
            ++level;
            cout << "-" << m_fileName << endl;
            // 多叉树的前序遍历
            for (FileDirAbstractSystem* p : m_listChildFile)
            {
                p->ShowName(level);
            }
        }
        virtual void AddChild(FileDirAbstractSystem* pSystem) override
        {
            m_listChildFile.push_back(pSystem);
            return;
        }
        virtual void RemoveChild(FileDirAbstractSystem* pSystem) override
        {
            m_listChildFile.remove(pSystem);
            return;
        }
    private:
        string m_fileName;  // 目录名字
        list<FileDirAbstractSystem*> m_listChildFile;  // 当前目录下的文件或目录
    };
    void test()
    {
        FileDirAbstractSystem* pDir = new Dir("E:");
        FileDirAbstractSystem* file1 = new File("1.txt");
        FileDirAbstractSystem* file2 = new File("2.txt");
        FileDirAbstractSystem* file3 = new File("3.txt");
        pDir->AddChild(file1);
        pDir->AddChild(file2);
        pDir->AddChild(file3);
        FileDirAbstractSystem* pDir2 = new Dir("新建文件夹");
        FileDirAbstractSystem* file21 = new File("1.txt");
        FileDirAbstractSystem* file22 = new File("2.txt");
        pDir2->AddChild(file21);
        pDir2->AddChild(file22);
        pDir->AddChild(pDir2);
        // 开始遍历
        pDir->ShowName(1);
        /*
            -E:
               -1.txt
               -2.txt
               -3.txt
               -新建文件夹
                  -1.txt
                  -2.txt
        */
    }
        引入组合(Composite)模式
引人"组合"设计模式的定义:将一组对象(文件和目录)组织成树形结构以表示"部分-整体"的层次结构,比如,目录中包含文件和子目录。使得用户对单个对象(文件)和组合对象(目录)的操作/使用/处理(递归遍历并执行 ShowName 逻辑等)具有一致性。
在上述定义中,用户是指 main 主函数中的调用代码,一致性指不用区分树叶还是树枝,两者都继承自 FileDirAbstractSystem类,两者都具有相同的接口,可以做相同的调用。可以看到,组合模式的设计思路其实就是用树形结构来组织数据,然后通过在树中递归遍历各个节点,并在每节点上统一地调用某个接口(例如,都调用 ShowName成员函数)或进行某些运算。
总之,组合模式之所以称为结构型模式,是因为该模式提供了一个结构,可以同时包容单个对象和组合对象。组合模式发挥作用的前提是具体数据必须能以树形结构的方式表示,树中包含了单个对象和组合对象。该模式专注于树形结构中单个对象和组合对象的递归遍历(只有递归遍历才能体现出组合模式的价值),能把相同的操作(FileDirAbstractSystem定义的接口)应用在单个以及组合对象上,并且可以忽略单个对象和组合对象之间的差别。从模式命名上,书中的作者认为命名成组合模式其实并不太恰当,命名成树形模式似乎更好。针对前面的代码范例绘制组合模式的 UML图,如下图所示:

组合模式UML包含的3种角色:
1 抽象组建:为树枝和树叶定义接口,可以是抽象类,包含所有子类公共行为的声明和默认实现体,这里指FileDirAbstractSystem类。
2 叶子组建:用于表示树叶节点对象,这种对象没有叶子节点,因此抽象组建中定义的一些接口,实际在这里没有实现的意义。
3 树枝组建:用一个容器节点对象,可以包含子节点,子节点可以是树枝或树叶,其中定义了一个集合用于抽象子节点。
组合模式的优点:
(1)使客户端可以忽略单个对象与组合对象的层次差异,客户端可以一致地使用单个对象和组合对象(客户端不知道也不关心处理的是单个对象还是组合对象),简化了代码的书写。
(2)无论是增加新的叶子组件还是树枝组件,都很方便,只需要增加继承自抽象组件的新类即可,这也符合开闭原则。
(3)组合模式为树形结构的面向对象实现方式提供了一种灵活的解决方案。通过对单个对象以及组合对象的递归遍历,可以处理复杂的树形结构。
其他使用组合模式的场景探讨
组合模式的使用场景:
1 公司的组织结构采用组合模式,比如人力资源,财务部,技术部等作为叶子组件来分别创建继承自一个抽象类。

可以利用组合模式这种"牵一发而动全身"的能力,完成诸如想所有部门发送通告,统计所有部门人数,计算整个公司的薪资成本等。
2 对某个目录下面的所有文件杀毒,文件的目录就可以用组合模式,然后可以使用一个KillVirus()来实现查杀。