C++设计模式_05_Observer 观察者模式

2023-09-12 22:43:59

接上篇,本篇将会介绍C++设计模式中的Observer 观察者模式,和前2篇模板方法Template MethodStrategy 策略模式一样,仍属于“组件协作”模式。Observer 在某些领域也叫做 Event

1. 动机( Motivation)

  • 在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系” ——一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密,将使软件不能很好地抵御变化。
  • 使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合。

2. 代码演示Observer 观察者模式

假设以下的场景需求:做一个文件的分割器。虽然现在文件分割器使用的比较少,但是在之前是使用很广泛的,因为当时还是一个3寸盘的时代,经常需要将大文件拷走,就需要将大的文件分隔为多个文件拷贝携带走。
下面是一个伪码,只会展示主干部分。

2.1 常用处理方法

首先需要一个界面,mainform就是一个windows界面,父类为Form,主要有两个控件txtFilePath(大文件的全路径)和txtFileNumber(希望分割的文件个数)(此处给出的是后期修改后的代码)。
Button1_Click函数中收集用户输入的2个参数信息,传递给FileSplitter splitter,splitter调用split()

2.1.1 MainForm1.cpp

class MainForm : public Form
{
	TextBox* txtFilePath;
	TextBox* txtFileNumber;
	ProgressBar* progressBar;

public:
	void Button1_Click(){

		string filePath = txtFilePath->getText();
		int number = atoi(txtFileNumber->getText().c_str());

		FileSplitter splitter(filePath, number, progressBar);

		splitter.split();

	}
};

2.1.2 FileSplitter1.cpp

FileSplitter中放文件变量,m_filePath负责文件路径,m_fileNumber负责文件个数,通过构造器给这些成员变量赋值。

class FileSplitter
{
	string m_filePath;
	int m_fileNumber;
	ProgressBar* m_progressBar; //具体的通知控件

public:
	FileSplitter(const string& filePath, int fileNumber, ProgressBar* progressBar) :
		m_filePath(filePath), 
		m_fileNumber(fileNumber),
		m_progressBar(progressBar){

	}

    //伪代码
	void split(){

		//1.读取大文件

		//2.分批次向小文件中写入
		for (int i = 0; i < m_fileNumber; i++){
			//...
			float progressValue = m_fileNumber;
			progressValue = (i + 1) / progressValue;
			m_progressBar->setValue(progressValue);
		}

	}
};

上面代码就实现了在Button1_Click时的文件的分割功能。

假设有一个用户需求,希望进行文件分割时,如果文件特别大,需要分割很长时间,这个时候需要提供一个进度条,进行进度展示。
最朴素的想法就是在界面中提供一个ProgressBar* progressBar,并且在FileSplitter中提供方法,代码结果如上。

但是上面的实现方式是否违背了某一设计原则呢?即违背依赖倒置原则(DIP)

  • 高层模块(稳定)不应该依赖于低层模块(变化),二者都应该依赖于抽象(稳定) 。
  • 抽象(稳定)不应该依赖于实现细节(变化) ,实现细节应该依赖于抽象(稳定)。

需要注意:一般我们所讲的依赖就是编译式依赖(除非明确提出运行式依赖),例如A依赖B也就是A编译时B必须存在

上面实现中,FileSplitter中ProgressBar* progressBar就产生了编译式依赖,这个ProgressBar* progressBar就是依赖倒置原则(DIP)中讲到的实现细节(为什么说它是实现细节呢?因为进度的显示方式可能会有变化,这就带了实现细节层面变更的困扰)

2.2 Observer 观察者模式

需要对上面的代码进行重构分析,可以看到ProgressBar* progressBar实际扮演的是一个通知,可以使用一种抽象的方式来表达一个通知而不需要具体的控件表达通知。

2.2.1 FileSplitter2.cpp

class IProgress{
public:
	virtual void DoProgress(float value)=0;
	virtual ~IProgress(){}
};


class FileSplitter
{
	string m_filePath;
	int m_fileNumber;

	List<IProgress*>  m_iprogressList; // 抽象通知机制,支持多个观察者,观察的就是分割的进度
	
public:
	FileSplitter(const string& filePath, int fileNumber) :
		m_filePath(filePath), 
		m_fileNumber(fileNumber){

	}


	void split(){

		//1.读取大文件

		//2.分批次向小文件中写入
		for (int i = 0; i < m_fileNumber; i++){
			//...

			float progressValue = m_fileNumber;
			progressValue = (i + 1) / progressValue;
			onProgress(progressValue);//发送通知
		}

	}

    //增加观察者
	void addIProgress(IProgress* iprogress){
		m_iprogressList.push_back(iprogress);
	}
    //移除观察者
	void removeIProgress(IProgress* iprogress){
		m_iprogressList.remove(iprogress);
	}


protected:
	//更新进度通知
	virtual void onProgress(float value){
		
		List<IProgress*>::iterator itor=m_iprogressList.begin();

		while (itor != m_iprogressList.end() )
			(*itor)->DoProgress(value); //更新进度条
			itor++;
		}
	}
};

2.2.2 MainForm2.cpp

C++中支持多继承,一般不推荐使用多继承的方式,可能会导致复杂的耦合性问题,但是C++推荐一种多继承的形式就是一个是主继承类(如public Form),其他是接口或者抽象基类(public IProgress)。

class MainForm : public Form, public IProgress
{
	TextBox* txtFilePath;
	TextBox* txtFileNumber;

	ProgressBar* progressBar;

public:
	void Button1_Click(){

		string filePath = txtFilePath->getText();
		int number = atoi(txtFileNumber->getText().c_str());

		ConsoleNotifier cn;

		FileSplitter splitter(filePath, number);

		splitter.addIProgress(this); //订阅通知
		splitter.addIProgress(&cn)//订阅通知

		splitter.split();

		splitter.removeIProgress(this);

	}

	virtual void DoProgress(float value){
		progressBar->setValue(value);
	}
};

class ConsoleNotifier : public IProgress {
public:
	virtual void DoProgress(float value){
		cout << ".";
	}
};

3. 模式定义

定义对象间的一种一对多(变化)的依赖关系,以便当一个对象(Subject)的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。 ——《设计模式》 GoF

4. 结构( Structure)

在这里插入图片描述

上图是《设计模式》GoF中定义的Observer 观察者模式的设计结构。结合上面的代码看图中最重要的是看其中稳定和变化部分,也就是下图中红框和蓝框框选的部分。

在这里插入图片描述

GoF的设计模式中建议将addIProgress,removeIProgress,onProgress放到父类中,让FileSplitter去继承父类。而此处我们的框架是将其直接写到FileSplitter中,不管是否提出Subject都是观察者模式,本博文重构的代码就没有提出Subject,其实是将Subject和ConcreteSubject合二为一。

个人结合结构对以上代码的理解:

IProgress的两个基类MainForm和ConsoleNotifier(虚函数多态,ConcreteObserver),通过FileSplitter splitter(ConcreteSubject)的addIProgress()方法都订阅了通知,在进行split()时,遍历ConcreteObserver,利用迭代器循环执行各自具体的DoProgress()。这样就实现多个ConcreteObserver观察同一ConcreteSubject,并且多个ConcreteObserver会根据ConcreteSubject变化执行各自的方法

5. 要点总结

  • 使用面向对象的抽象, Observer模式使得我们可以独立地改变目标与观察者,从而使二者之间的依赖关系达致松耦合。

代码中随便添加观察者,但是addIProgress,removeIProgress,onProgress保持复用性不变

  • 目标发送通知时,无需指定观察者,通知(可以携带通知信息作为参数)会自动传播。

onProgress(progressValue);//发送通知不知道谁是观察者,针对通知机制抽象通知

  • 观察者自己决定是否需要订阅通知,目标对象对此一无所知。

splitter.addIProgress(this); //订阅通知splitter.addIProgress(&cn); //订阅通知

  • Observer模式是基于事件的UI框架中非常常用的设计模式,也是MVC模式的一个重要组成部分。

Observer 观察者模式与模板方法Template Method是一样的常用。例如java中的listener就是观察者模式、C#中的Event也是观察者模式

Observer模式需要多思考,最关键的是抽象的通知依赖关系。

6. 其他参考博文

Observer 观察者模式

更多推荐

【Mybatis】基础部分

mybatis持久层:可以立即保存在磁盘上,在这里可以理解为与数据库相关操作。持久层技术解决方案有几种?1.JDBC技术–>Connection、PreparedStatement、ResultSet2.Spring的JdbcTemplate–>Spring中对Jdbc的简单封装3.Apache(阿帕奇)的DBUtil

从零实现带RLHF的类ChatGPT:逐行解析微软DeepSpeed Chat

写在最前面本文最早写于2023年4月的这篇文章中《从零实现带RLHF的类ChatGPT:从TRL/ChatLLaMA/ColossalChat到DeepSpeedChat》,后因要在「大模型项目开发线下营」上讲DSC的实现而不断扩写其中的DSC,为避免原文过长,故把该文最后的DSC部分抽取出来成本文前言如此文所述,微软

浅谈基于物联网的医院消防安全管理

安科瑞华楠摘要:医院消防物联网将原本与网络无关的消防设施和网络结合起来,将消防监督管理、防火灭火所需的相关信息进行汇总,可以让医院更加轻松地发现和处理医院的警情信息,降低火灾发生频率。关键词:物联网技术;医院智慧消防;1医院火灾的特点医院是人员密集场所,发生火灾时,容易出现踩踏等问题,进而严重威胁群众的生命财产。医院拥

渗透测试——formatworld(1)

文章目录一、环境二、获取flag11、扫描局域网内存活主机1.1查看kali的IP地址1.2扫描存活主机2、粗略扫描靶机端口(服务)3、寻找ftp服务漏洞4、扫描端口详细信息5、匿名登录ftp一、环境攻击机:kali靶机:formatworld二、获取flag11、扫描局域网内存活主机1.1查看kali的IP地址ifc

Redis面试题(一)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档文章目录前言一、什么是Redis?简述它的优缺点?二、Redis与memcached相比有哪些优势?三、Redis支持哪几种数据类型?四、Redis主要消耗什么物理资源?五、Redis有哪几种数据淘汰策略?六、Redis官方为什么不提供Windows版本?

Springboot 外部化的配置

SpringBoot可以让你将配置外部化,这样你就可以在不同的环境中使用相同的应用程序代码。你可以使用各种外部配置源,包括Javaproperties文件、YAML文件、环境变量和命令行参数。属性值可以通过使用@Value注解直接注入你的Bean,也可以通过Spring的Environment访问,或者通过@Confi

网络安全(黑客)自学

一、前言:1.这是一条坚持的道路,三分钟的热情可以放弃往下看了.2.多练多想,不要离开了教程什么都不会了.最好看完教程自己独立完成技术方面的开发.3.有时多google,baidu,我们往往都遇不到好心的大神,谁会无聊天天给你做解答.4.遇到实在搞不懂的,可以先放放,以后再来解决.想自学网络安全(黑客技术)首先你得了解

电力和水利工程行业浪涌保护器的选型方案

电力和水利工程行业是国民经济的重要支柱,其设备和系统的安全稳定运行对社会和人民生活有着重要意义。然而,这些行业也面临着雷电等自然灾害的威胁,雷电过电压会造成电力设备的损坏、故障、停运甚至火灾爆炸等严重后果。因此,采用合适的浪涌保护器(SPD)是防止雷电危害的有效措施之一。地凯科技浪涌保护器是一种能够在瞬间将雷电过电压泄

@RequestMapping 注解以及其它使用方式

😀前言本篇主要讲解@RequestMapping注解以及其它使用方式🏠个人主页:尘觉主页🧑个人简介:大家好,我是尘觉,希望我的文章可以帮助到大家,您的满意是我的动力😉😉在csdn获奖荣誉:🏆csdn城市之星2名⁣⁣⁣⁣⁣⁣⁣⁣⁣⁣⁣⁣⁣⁣⁣⁣⁣⁣⁣⁣⁣⁣⁣⁣⁣⁣⁣⁣⁣⁣⁣⁣💓Java全栈群星计划top前5

JVM基础-Hotspot VM相关知识学习

这里写目录标题jdkJVM虚拟机类类的生命周期类加载的时机类的双亲委派机制类的验证java对象MarkWordKlassPointer实例数据对齐数据字符串常量池垃圾收集器1.Serial收集器(串行收集器)cms垃圾算法G1垃圾收集器与CMS收集器相比,G1收集器的优势:G1收集器的实现原理:JVM参考文章:JVM之

30.链表练习题(1)(王道2023数据结构2.3.7节1-8题)

【前面使用的所有链表的定义在第29节】试题1:设计一个递归算法,删除不带头结点的单链表L中所有值为x的结点。首先来看非递归算法,暴力遍历:intDel(LinkList&L,ElemTypex){//此函数实现删除链表中为x的元素LNode*p,*q;p=L;//p指向头结点q=L->next;//q指向首元结点whi

热文推荐