一、组合模式是什么?
组合模式是一种结构型的软件设计模式,将对象组合成树形结构,以凸显“部分-整体”的层次结构,使客户端对单个对象和组合对象的操作具备一致性。
组合模式和桥接模式都应用了组合的思想,不同之处在于:桥接模式侧重于同级别间的组合,如多个属性的组合,避免了类爆炸;组合模式侧重于部分和整体的组合,避免了单对象和组合对象的区别对待,那样会增加程序复杂度。
组合模式的优点:
- 层次鲜明。凸显“部分-整体”的层次结构。
- 一致性。对叶子对象(单)和容器对象(组合)的操作具备良好一致性。
- 节点自由度高。在结构中按需自由添加节点。
组合模式的缺点:
- 设计更抽象。
- 应用场景限制。
组合模式一般分为透明式组合模式和安全式组合模式。
1)透明式组合模式:将公共接口封装到抽象节点中,所有节点具备一致行为。
2)安全式组合模式:各层次差异较大,使用不同操作时建议采用该模式。
二、透明式组合模式
2.1 结构图
客户端即Main主函数,采用透明式,无需分辨节点是叶子还是容器,只需使用抽象节点。
2.2 代码示例
场景描述:我要设计一个文件系统,在文件夹中可以添加删除文件或者文件夹。
//File.h /****************************************************/ #pragma once #include <iostream> #include <list> using namespace std; // 抽象类-节点 class Node { public: // 构造函数 explicit Node(string name) :m_name(name) {}; // 析构函数 virtual ~Node() {}; // 添加 virtual void add(Node *node) {}; // 删除 virtual void remove(Node *node) {}; // 显示 virtual void show(int space) { for (int i = 0; i < space; i++) { cout << " "; } cout << m_name << endl; } protected: string m_name; // 名字 }; // 具体类-Word文件 class WordFile :public Node { public: // 构造函数 explicit WordFile(string name) :Node(name) {}; // 析构函数 virtual ~WordFile() {}; }; // 具体类-文件夹 class Folder :public Node { public: // 构造函数 explicit Folder(string name) :Node(name) {}; // 析构函数 virtual ~Folder() { nodeList.clear(); } // 添加 virtual void add(Node *node) { nodeList.emplace_back(node); } // 删除 virtual void remove(Node *node) { nodeList.remove(node); } // 显示 virtual void show(int space) { Node::show(space); space++; for (auto node : nodeList) { node->show(space); } } private: list<Node*> nodeList; // 节点列表 };
//main.cpp /****************************************************/ #include <iostream> #include <string> #include "File.h" using namespace std; int main() { Node *f0 = new Folder("我的文件夹"); // 文件夹1中放入Word2和Word3,并将文件夹1放入我的文件夹 Node *f1 = new Folder("文件夹1"); Node *w2 = new WordFile("Word2"); Node *w3 = new WordFile("Word3"); f1->add(w2); f1->add(w3); f0->add(f1); // 将Word1放入我的文件夹 Node *w1 = new WordFile("Word1"); f0->add(w1); // 显示我的文件夹中的内容 f0->show(0); // 删除文件夹1中的Word2文件,再次显示我的文件夹中的内容 f1->remove(w2); f0->show(0); // 删除指针并置空 delete f0, f1, w1, w2, w3; f0 = nullptr; f1 = nullptr; w1 = nullptr; w2 = nullptr; w3 = nullptr; return 0; }
程序结果如下。
在上述示例中,我采用的都是抽象节点,因为抽象类中涵盖了几乎所有的行为。对外界(客户端)而言,叶子和容器的行为没有区别,它们的区别是不可见的也是透明的。但同样,这样的设计一定意义上也是不安全的,因为叶子不可能和容器有完全一致的行为,就像上文中文件夹的增删功能,文件是没有的,如果对文件进行文件夹的相关独特操作,要做相关的异常判断。
该模式是组合模式中常用的,虽然它违背了设计模式中的接口隔离原则(只提供需要的接口,不需要的接口要屏蔽)。
三、安全式组合模式
3.1 结构图
客户端即Main主函数,采用安全式,抽象类中只规定基础操作,而叶子和容器各自独有的操作将放在自身中完成,因此客户端在调用时无法直接使用抽象节点。
3.2 代码示例
场景描述:我要设计一个文件系统,在文件夹中可以添加删除文件或者文件夹。
//File.h /****************************************************/ #pragma once #include <iostream> #include <list> using namespace std; // 抽象类-节点 class Node { public: // 构造函数 explicit Node(string name) :m_name(name) {}; // 析构函数 virtual ~Node() {}; // 显示 virtual void show(int space) = 0; protected: string m_name; // 名字 }; // 具体类-Word文件 class WordFile :public Node { public: // 构造函数 explicit WordFile(string name) :Node(name) {}; // 析构函数 virtual ~WordFile() {}; // 显示 virtual void show(int space) { for (int i = 0; i < space; i++) { cout << " "; } cout << m_name << endl; } }; // 具体类-文件夹 class Folder :public Node { public: // 构造函数 explicit Folder(string name) :Node(name) {}; // 析构函数 virtual ~Folder() { nodeList.clear(); } // 添加 void add(Node *node) { nodeList.emplace_back(node); } // 删除 void remove(Node *node) { nodeList.remove(node); } // 显示 virtual void show(int space) { for (int i = 0; i < space; i++) { cout << " "; } cout << m_name << endl; space++; for (auto node : nodeList) { node->show(space); } } private: list<Node*> nodeList; // 节点列表 };
//main.cpp /****************************************************/ #include <iostream> #include <string> #include "File.h" using namespace std; int main() { Folder *f0 = new Folder("我的文件夹"); // 文件夹1中放入Word2和Word3,并将文件夹1放入我的文件夹 Folder *f1 = new Folder("文件夹1"); WordFile *w2 = new WordFile("Word2"); WordFile *w3 = new WordFile("Word3"); f1->add(w2); f1->add(w3); f0->add(f1); // 将Word1放入我的文件夹 WordFile *w1 = new WordFile("Word1"); f0->add(w1); // 显示我的文件夹中的内容 f0->show(0); // 删除文件夹1中的Word2文件,再次显示我的文件夹中的内容 f1->remove(w2); f0->show(0); // 删除指针并置空 delete f0, f1, w1, w2, w3; f0 = nullptr; f1 = nullptr; w1 = nullptr; w2 = nullptr; w3 = nullptr; return 0; }
程序结果如下。
在上述示例中,客户端无法直接使用抽象节点了,因为抽象节点中没有add和remove的操作,但这样更安全了,叶子和容器不会调用对方的功能进而触发异常。
注意:该模式违背了依赖倒置原则(程序设计要依赖于抽象接口,不要依赖于具体实现)。
四、总结
我尽可能用较通俗的话语和直观的代码例程,来表述我对组合模式的理解,或许有考虑不周到的地方,如果你有不同看法欢迎评论区交流!希望我举的例子能帮助你更好地理解组合模式。
如果文章帮助到你了,可以点个赞让我知道,我会很快乐~加油!