分享

设计模式之组合模式(C++)

 翟天保的图书馆 2023-03-30 发布于上海

作者:翟天保Steven
版权声明:著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处

一、组合模式是什么?

       组合模式是一种结构型的软件设计模式,将对象组合成树形结构,以凸显“部分-整体”的层次结构,使客户端对单个对象和组合对象的操作具备一致性。

       组合模式和桥接模式都应用了组合的思想,不同之处在于:桥接模式侧重于同级别间的组合,如多个属性的组合,避免了类爆炸;组合模式侧重于部分和整体的组合,避免了单对象和组合对象的区别对待,那样会增加程序复杂度。

       组合模式的优点:

  1. 层次鲜明。凸显“部分-整体”的层次结构。
  2. 一致性。对叶子对象(单)和容器对象(组合)的操作具备良好一致性。
  3. 节点自由度高。在结构中按需自由添加节点。

       组合模式的缺点:

  1. 设计更抽象。
  2. 应用场景限制。

       组合模式一般分为透明式组合模式和安全式组合模式。

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的操作,但这样更安全了,叶子和容器不会调用对方的功能进而触发异常。

       注意:该模式违背了依赖倒置原则(程序设计要依赖于抽象接口,不要依赖于具体实现)。

四、总结

       我尽可能用较通俗的话语和直观的代码例程,来表述我对组合模式的理解,或许有考虑不周到的地方,如果你有不同看法欢迎评论区交流!希望我举的例子能帮助你更好地理解组合模式。

       如果文章帮助到你了,可以点个赞让我知道,我会很快乐~加油!

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多