C++设计模式笔记(05) - Observer 观察者模式



  • 参考课程:《C++设计模式》-李建忠
    李建忠-C++设计模式

1.动机(Motivation)

▷将一个系统分割成一系列相互协作的类有一个常见的副作用:需要维护相关对象间的一致性。我们不希望为了维持一致性而使各类紧密耦合,因为这样降低了它们的可复用性

▷在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系” ——一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密,将使软件不能很好地抵御变化

▷使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合
在这里插入图片描述

2.实例:

  1. 例如, 许多图形用户界面工具箱将用户应用的界面表示与底下的应用数据分离。定义应用数据的类和负责界面表示的类可以各自独立地复用。当然它们也可一起工作。一个表格对象和一个柱状图对象可使用不同的表示形式描述同一个应用数据对象的信息。表格对象和柱状图对象互相并不知道对方的存在,这样使你可以根据需要单独复用表格或柱状图。但在这里是它们表现的似乎互相知道。当用户改变表格中的信息时,柱状图能立即反映这一变化, 反过来也是如此。
    在这里插入图片描述
    这一行为意味着表格对象和柱状图对象都依赖于数据对象, 因此数据对象的任何状态改变都应立即通知它们。同时也没有理由将依赖于该数据对象的对象的数目限定为两个, 对相同的数据可以有任意数目的不同用户界面。
  2. 以文件分割器为例:
    1)一般解决方法:
//伪代码,未严格遵循cpp编码标准
//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();
	}
};
//伪代码,未严格遵循cpp编码标准
//FileSplitter1.cpp
class FileSplitter
{
	string m_filePath;	//文件路径
	int m_fileNum ber;	//文件个数
	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);	//更新进度条
		}
	}
};

2)观察者模式解决方法:

//伪代码,未严格遵循cpp编码标准
//MainForm2.cpp
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 << ".";
	}
};
//伪代码,未严格遵循cpp编码标准
//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++;
		}
	}
};

3.模式定义

在这里插入图片描述
定义对象间的一种一对多(变化)的依赖关系,以便当一个对象(Subject)的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。
             ——《设计模式:可复用面向对象软件的基础》

4.结构

在这里插入图片描述

5.要点总结

在这里插入图片描述
▷使用面向对象的抽象,Observer模式使得我们可以独立地改变目标与观察者,从而使二者之间的依赖关系达致松耦合。

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

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

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

6.《Head First 设计模式》实例的C++实现

1)场景

我们接到一个来自气象局的需求:气象局需要我们构建一套系统,这系统有两个布告板,分别用于显示当前的实时天气和未来几天的天气预报。当气象局发布新的天气数据(WeatherData)后,两个布告板上显示的天气数据必须实时更新。气象局同时要求我们保证程序拥有足够的可扩展性,因为后期随时可能要新增新的布告板。

2)概况

这套系统中主要包括三个部分:气象站(获取天气数据的物理设备)、WeatherData(追踪来自气象站的数据,并更新布告板)、布告板(用于展示天气数据)

WeatherData知道如何跟气象站联系,以获得天气数据。当天气数据有更新时,WeatherData会更新两个布告板用于展示新的天气数据。
在这里插入图片描述

7)代码:
  • 主题(发布者\被观察者)接口(Subject):
//subject.h
#ifndef SUBJECT_H
#define SUBJECT_H


#pragma once

//主题接口

//声明观察者类
class Observer;

class Subject
{
public:
    Subject(void);

    virtual void registerObserver(Observer* o) = 0;
    virtual void removeObserver(Observer* o) = 0;

    //当主题状态改变时,这个方法会被调用,以通知所有的观察者
    virtual void notifyObserver() = 0;

    ~Subject(void);
};
#endif // SUBJECT_H
//subject.cpp
#include "subject.h"

Subject::Subject(void)
{
}

Subject::~Subject(void)
{
}
  • 观察者接口(Observer):
//observer.h
#ifndef OBSERVER_H
#define OBSERVER_H


#pragma once

//观察者接口,此例中用于更改数据

class Observer
{
public:
    Observer(void);

    virtual void update(float temp, float humidity, float pressure) = 0;

    ~Observer(void);
};
#endif // OBSERVER_H
//observer.cpp
#include "observer.h"

Observer::Observer(void)
{
}

Observer::~Observer(void)
{
}
  • 布告板用于显示的公共接口(DisplayElement):
//displayelement.h
#ifndef DISPLAYELEMENT_H
#define DISPLAYELEMENT_H

#pragma once

//显示接口,在C++中使用多重继承完成此类目的没什么必要,但为了和原著相符,这里还是定义了该接口

class DisplayElement
{
public:
    DisplayElement(void);

    //当布告板需要显示时,调用此方法
    virtual void display() = 0;

    ~DisplayElement(void);
};
#endif // DISPLAYELEMENT_H
//displayelement.cpp
#include "displayelement.h"

DisplayElement::DisplayElement(void)
{
}

DisplayElement::~DisplayElement(void)
{
}
  • 下面我们再来看看WeatherData是如何实现的:
//weatherdata.h
#ifndef WEATHERDATA_H
#define WEATHERDATA_H


#pragma once
#include "subject.h"
#include "observer.h"
#include <vector>
#include <algorithm>

using namespace std;

//实现主题接口

class WeatherData : public Subject
{
private:
    vector<Observer *> observers;   //用vector容器记录观察者,在构造函数中初始化
    float temperature;  //温度
    float humidity;     //湿度
    float pressure;     //气压

public:
    WeatherData(void);

    void registerObserver(Observer *o);     //注册观察者
    void removeObserver(Observer *o);       //取消观察者
    void notifyObserver();                  //通知观察者

    void measurementsChanged();    //当从气象站取得新的观测值时,我们通知观察者
    void setMeasurements(float t, float h, float p);  //测试方法
    ~WeatherData(void);
};
#endif // WEATHERDATA_H
//weatherdata.cpp
#include "weatherdata.h"

WeatherData::WeatherData(void)
{
    observers = vector<Observer *>();
}

WeatherData::~WeatherData(void)
{
}

void WeatherData::registerObserver(Observer *o)
{
    observers.push_back(o);
}

void WeatherData::removeObserver(Observer *o)
{
    vector<Observer *>::iterator iter = find(observers.begin(), observers.end(), o);
    if (iter != observers.end())
        observers.erase(iter);
}

void WeatherData::notifyObserver()
{
    for (int i=0; i<observers.size(); i++)
    {
        observers[i]->update(temperature, humidity, pressure);
    }
}

void WeatherData::measurementsChanged()
{
    notifyObserver();
}

void WeatherData::setMeasurements(float t, float h, float p)
{
    temperature = t;
    humidity = h;
    pressure = p;
    measurementsChanged();
}
  • 显示当前天气的公告牌CurrentConditionsDisplay
//currentconditionsdisplay.h
#ifndef CURRENTCONDITIONDISPLAY_H
#define CURRENTCONDITIONDISPLAY_H

//建立布告板
#pragma once
#include "observer.h"
#include "subject.h"
#include "displayelement.h"
#include <iostream>

using namespace std;

//具体观察者,和书上一样,在此只实现其中一个布告板
//此布告板实现了Observer接口,所以可以从WetherData对象中获得改变。
//它也实现了DisplayElement接口,因此我们规定所有的布告板都必须实现此接口
class CurrentConditionDisplay : public Observer, public DisplayElement
{
private:
    float temperature;
    float humidity;
    Subject* weatherData;

public:
    //构造器需要Weather对象(也就是主题)作为注册之用
    CurrentConditionDisplay(Subject* w);

    void update(float temp, float humidity, float pressure);
    void display();

    ~CurrentConditionDisplay(void);
};
#endif // CURRENTCONDITIONDISPLAY_H
//currentconditiondisplay.cpp
#include "currentconditiondisplay.h"

CurrentConditionDisplay::CurrentConditionDisplay(Subject *w)
{
    weatherData = w;
    weatherData ->registerObserver(this);
}

void CurrentConditionDisplay::update(float temp, float humidity, float pressure)
{
    this->temperature = temp;
    this->humidity = humidity;  //当upadate()被调用时, 我们把温度和湿度保存起来,然后调用display()。
    display();
}

//display()方法就只是把最近的温度和湿度显示出来
void CurrentConditionDisplay::display()
{
    cout << "Current conditions: " << temperature
         << "F degrees and " << humidity << "% humidity" << endl;
}

CurrentConditionDisplay::~CurrentConditionDisplay(void)
{
}
  • 好了,我们测试下利用观察者模式重构后的程序:
//观察者模式:定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有者都会收到通知并自动更新

//OO原则:为交互对象之间的松耦合设计而努力

//main.cpp
#include "weatherdata.h"
#include "currentconditiondisplay.h"

//测试
int main()
{
    //启动气象站

    //建立主题对象
    WeatherData weatherData;

    //建立观察者对象
    CurrentConditionDisplay currentDisplay(&weatherData);

    weatherData.setMeasurements(80, 65, 30.4);
    weatherData.setMeasurements(82, 70, 29.2);
    weatherData.setMeasurements(78, 90, 29.2);

    return 0;
}

欢迎关注公众号:c_302888524
发送:“设计模式:可复用面向对象软件的基础” 获取电子书
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值