实战设计模式之组合模式
概述
组合模式是一种结构型设计模式,允许我们将对象组合成树形结构,以表示部分和整体的层次关系。通过这种方式,我们可以统一地处理单个对象和对象组合。换句话说,组合模式使得客户代码能够忽略对象的层次结构,以一致的方式对待个体和集合。
文件系统是运用组合模式的一个典型例子:计算机上的文件夹(或目录)可以包含其他文件夹或文件,而文件夹本身又可以被包含在更大的文件夹中。这种层次化的结构,允许用户以一致的方式来处理单个文件和整个文件夹。
基本原理
组合模式的主要目标是:让客户端能够透明地处理单个对象和对象组合。为了实现这一点,组合模式通常定义了一个接口或抽象类,所有叶子节点(即不可再分的对象)和组合节点(即包含其他对象的对象)都实现了这个接口或继承了这个抽象类。这样一来,无论是叶子节点还是组合节点,它们对外呈现的接口都是相同的,从而实现了“同质化”。组合模式主要由以下四个核心组件构成。
1、组件接口。这是组合模式中最顶层的接口,定义了所有组件共有的操作和属性。无论是叶子节点还是组合节点,都必须实现这些方法,这使得客户端代码可以统一地处理单个对象和对象组合。
2、叶子节点。叶子节点是不可再分的基本单元,它实现了组件接口中定义的方法,但不会包含任何子组件。叶子节点代表的是最底层的对象,比如:文件系统中的具体文件、组织架构中的员工等。
3、组合节点。组合节点是包含其他组件(可以是叶子节点或其他组合节点)的对象。它同样实现了组件接口,并且负责管理其内部的子组件集合。通过这种方式,组合节点可以递归地构建更复杂的结构。
4、客户端。客户端通过组件接口与所有类型的组件交互,而无需关心具体是叶子节点还是组合节点。这种透明性,简化了客户端的设计和实现。
基于上面的核心组件,组合模式的实现主要有以下四个步骤。
1、定义组件接口。创建一个接口,声明所有组件都应该实现的方法。这包括基本操作,以及可能的组合相关操作(如果适用的话)。对于那些只适用于组合节点的方法,可以在接口中定义默认实现,以便叶子节点可以选择性地忽略它们。
2、实现叶子节点。为每一个不可再分的基本单位创建具体的叶子类,并实现组件接口中定义的方法。由于叶子节点不包含子组件,所以与组合有关的操作可以直接抛出异常或留空。
3、实现组合节点。创建一个组合类,该类不仅实现了组件接口,还提供了管理子组件的方法。组合节点需要维护一个子组件列表,并在适当的时候调用子组件的相关方法。
4、编写客户端代码。客户端代码应当尽可能地依赖组件接口,而不是具体的实现类。这样做的好处是可以轻松地替换或扩展不同类型的组件,而无需修改客户端逻辑。
实战解析
在下面的实战代码中,我们使用组合模式模拟了文件系统的树状层次结构。
首先,我们定义了组件接口类CComponent。它是所有组件的基类,声明了四个纯虚函数:Operation、Add、Remove、GetChild,以及一个非纯虚函数GetName。默认情况下,Add、Remove和GetChild抛出异常,因为并非所有组件都需要实现这些方法。
接下来,我们实现了叶子节点CFile。CFile类继承自CComponent,代表文件系统中的单个文件。它实现了 Operation方法以输出文件名,并实现了GetName方法以返回文件名。
同时,我们还实现了组合节点CFolder。CFolder类同样继承自CComponent,但与CFile不同的是,它可以包含其他CComponent对象(即文件或其他文件夹)。CFolder类实现了Operation方法,该方法会递归地调用其所有子组件的Operation方法。此外,它还实现了Add、Remove和GetChild方法,用于管理子组件集合。CFolder的析构函数负责清理所有子组件,防止内存泄漏。
最后,在main函数中,我们创建了几个文件和文件夹实例。先将两个文件添加到一个文件夹pFolder1中,再将这个文件夹与其他文件一起添加到另一个更大的文件夹pFolder2中。通过调用最外层文件夹pFolder2的Operation方法,我们递归地显示了整个文件系统的结构。
#include <iostream>
#include <string>
#include <vector>
using namespace std;
// 定义组件接口
class CComponent
{
public:
virtual ~CComponent() {}
virtual void Operation() const = 0;
virtual void Add(CComponent* pChild)
{
throw runtime_error("Not supported");
}
virtual void Remove(const string& name)
{
throw runtime_error("Not supported");
}
virtual CComponent* GetChild(int index)
{
throw runtime_error("Not supported");
}
virtual string GetName() const = 0;
};
// 实现叶子节点:文件
class CFile : public CComponent
{
public:
CFile(const string& strName) : m_strName(strName) {}
void Operation() const override
{
cout << "File: " << m_strName << endl;
}
string GetName() const override
{
return m_strName;
}
private:
string m_strName;
};
// 实现组合节点:文件夹
class CFolder : public CComponent
{
public:
CFolder(const string& strName) : m_strName(strName) {}
~CFolder()
{
// 清理所有子组件
for (auto pChild : m_vctChildren)
{
delete pChild;
}
}
void Operation() const override
{
cout << "Folder: " << m_strName << endl;
for (auto pChild : m_vctChildren)
{
pChild->Operation();
}
}
void Add(CComponent* pChild) override
{
m_vctChildren.push_back(pChild);
}
void Remove(const string& name) override
{
m_vctChildren.erase(remove_if(m_vctChildren.begin(), m_vctChildren.end(),
[&name](CComponent* pChild) {
return pChild->GetName() == name;
}),
m_vctChildren.end());
}
CComponent* GetChild(int index) override
{
if (index >= 0 && index < (int)m_vctChildren.size())
{
return m_vctChildren[index];
}
throw out_of_range("Index out of range");
}
string GetName() const override
{
return m_strName;
}
private:
string m_strName;
vector<CComponent*> m_vctChildren;
};
int main()
{
// 创建文件和文件夹实例
CComponent* pFile1 = new CFile("file1.txt");
CComponent* pFile2 = new CFile("file2.txt");
CComponent* pFolder1 = new CFolder("folder1");
CComponent* pFolder2 = new CFolder("folder2");
// 将文件添加到文件夹中
pFolder1->Add(pFile1);
pFolder1->Add(pFile2);
pFolder2->Add(new CFile("file3.txt"));
pFolder2->Add(pFolder1);
// 操作文件夹,自动递归操作其中的文件
pFolder2->Operation();
// 自动清理其包含的所有子组件
delete pFolder2;
return 0;
}
总结
组合模式支持开闭原则,即对扩展开放,对修改关闭。新的组件类型可以很容易地添加到系统中,而不会影响现有的代码。组合模式允许构建复杂的递归结构,比如:文件系统的目录和文件、组织架构中的部门和个人等,这样的结构能够很好地模拟现实世界中的分层关系。由于所有组件都实现了相同的接口,因此可以在不同的上下文中复用这些组件,并且可以根据需要动态地增加或移除子组件。
但如果组合节点承担了过多的责任(比如:管理子组件、执行业务逻辑等),可能会使类变得过于庞大和复杂,进而违背单一职责原则。为了防止这种情况发生,应该尽量保持每个类的职责单一明确。另外,对于大型树状结构,遍历所有节点可能会导致性能问题,尤其是在频繁调用的情况下。此时,可能需要优化访问模式,或者引入缓存机制。