C++设计模式之观察者模式(行为型模式)

学习软件设计,向OO高手迈进!
设计模式(Design pattern)是软件开发人员在软件开发过程中面临的一般问题的解决方案。
这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
是前辈大神们留下的软件设计的"招式"或是"套路"。

什么是观察者模式

在本文末尾会给出解释,待耐心看完demo再看定义,相信你会有更深刻的印象

实例讲解

背景

我们接到一个来自气象局的需求:气象局需要我们构建一套系统,这系统目前只有一个公告板,用于显示当前的实时天气情况。当气象局发布新的天气数据 (WeatherData) 后,该公告板上显示的天气数据必须实时更新。同时气象局要求我们保证该系统必须可扩展,因为后期随时可能要添加新的公告板。

这套系统中主要包括3个部分:
气象站:获取天气数据的物理设备
WeatherData 对象:追踪来自气象站的数据,并更新公告板
公告板:用于显示天气数据给用户看
在这里插入图片描述

WeatherData 对象由气象局提供,WeatherData 对象知道怎么跟气象站联系(我们不需要 care),以获得天气数据。当天气数据有更新时,WeatherData 对象会更新公告板用于显示新的天气数据。我们看一下 WeatherData 对象的类图
在这里插入图片描述

我们先来看看隔壁老王的实现

Version 1.0

WeatherData 类,这里为了方便测试,用随机函数返回温度、湿度、气压值

class WeatherData {
public:
    float GetTemperature(void) {
        srand((int)time(0) + 1);
        int temperature = rand() % 100;
        return (float)temperature;
    }
    float GetHumidity(void) {
        srand((int)time(0) + 2);
        int humidity = rand() % 100;
        return (float)humidity;
    }
    float GetPressure(void) {
        srand((int)time(0) + 3);
        int pressure = rand() % 1000;
        return (float)pressure;
    }
    void MeasurementsChanged(void) {
        float temperature = GetTemperature();
        float humidity = GetHumidity();
        float pressure = GetPressure();
        // 采用"推"的方式把数据传送过去
        m_objCurrentConditionsDisplay.Update(temperature, humidity, pressure);
    }
private:
    CurrentConditionsDisplay m_objCurrentConditionsDisplay;
}

CurrentConditionsDisplay 类

class CurrentConditionsDisplay {
public:
    CurrentConditionsDisplay() : m_fTemperature(0.0), m_fHumidity(0.0), m_fPressure(0.0) {}
    void Display(void) {
        printf("Current conditions: %.1f*C %.1f%% %.1fhPa\n", m_fTemperature, m_fHumidity, m_fPressure);
    }
    void Update(float temperature, float humidity, float pressure) {
        m_fTemperature = temperature;
        m_fHumidity = humidity;
        m_fPressure = pressure;
        Display();
    }
private:
    float m_fTemperature; // 温度
    float m_fHumidity;    // 湿度
    float m_fPressure;    // 气压
};

main 函数只需要调用 WeatherData 对象的 MeasurementsChanged() 即可测试

int main(int argc, char *argv[]) {
    WeatherData *weatherData = new WeatherData();
    weatherData->MeasurementsChanged();
    delete weatherData;
    return 0;
}

结果如下

Current conditions: 43.0*C 93.0% 793.0hPa

测试正常!不过上面这段代码是典型的针对实现编程,这会导致我们以后增加或删除公告板时必须修改程序(修改WeatherData 类)。假如气象局明天要求我们再增加一个公告板,用于显示未来几天的温度情况,那又怎么写呢?

我们现在来看看观察者模式,举例说明:

  1. 我们都知道报社的业务是出版报纸,假如小明向报社订阅报纸,只要报社有新报纸出版,报社就会给小明送来,只要小明还是报社的客户,小明就会每天都收到新报纸。当小明不想再看报纸的时候,取消订阅,报社就不会再送新报纸来。只要报社还在运营,就会一直有人向报社订阅报纸或取消订阅报纸

  2. 我们都在微博中关注过某一位明星,每当这位明星发布一条动态时候,他的粉丝就都会知道。当然,明星有权利让你关注,也有权利把你拉黑;粉丝也有权利取消关注

在观察者模式里,报社、明星被称为 Subject (主题),小明、粉丝称为 Observer (观察者)

思考改进

先来看看观察者的类图
在这里插入图片描述

结合上面的类图,现在将观察者模式应用到气象站项目中来。就有了下面这个类图
在这里插入图片描述

将类图转化为代码

Version 2.0

ISubject 主题接口

class ISubject {
public:
    virtual void RegisterObserver(IObserver *observer) = 0;
    virtual void RemoveObserver(IObserver *observer) = 0;
    virtual void NotifyObservers(void) = 0;
};

IObserver 观察者接口

class IObserver {
public:
    virtual void Update(void) = 0;
};

IDisplayElement 公告板用于显示的公共接口

class IDisplayElement {
public:
    virtual void Display(void) = 0;
};

WeatherData 类,用一个 vector 存放观察者们

class WeatherData : public ISubject {
public:
    WeatherData() {
        m_fTemperature = 0.0;
        m_fHumidity = 0.0;
        m_fPressure = 0.0;
        m_pObservers.clear();
    }
    void RegisterObserver(IObserver *observer) {
        m_pObservers.push_back(observer);
    }
    void RemoveObserver(IObserver *observer) {
        std::vector<IObserver*>::iterator it;
        it = std::find(m_pObservers.begin(), m_pObservers.end(), observer);
        if(it != m_pObservers.end()) {
            m_pObservers.erase(it);
        }
    }
    void NotifyObservers(void) {
        std::vector<IObserver*>::iterator it;
        for(it = m_pObservers.begin(); it != m_pObservers.end(); it++) {
            // 让观察者们采用"拉"的方式获取数据
            (*it)->Update();
        }
    }
    float GetTemperature(void) {
        srand((int)time(0) + 1);
        int temperature = rand() % 100;
        return (float)temperature;
    }
    float GetHumidity(void) {
        srand((int)time(0) + 2);
        int humidity = rand() % 100;
        return (float)humidity;
    }
    float GetPressure(void) {
        srand((int)time(0) + 3);
        int pressure = rand() % 1000;
        return (float)pressure;
    }
    void GetForecastTemperatures(std::vector<float> &temperatures) {
        srand((int)time(0) + 4);
        for(int i = 0; i < 5; i++) {
            float temp = (float)(rand() % 100);
            temperatures.push_back(temp);
        }
    }
    void MeasurementsChanged(void) {
        m_fTemperature = GetTemperature();
        m_fHumidity = GetHumidity();
        m_fPressure = GetPressure();
        NotifyObservers();
    }
private:
    float m_fTemperature;
    float m_fHumidity;
    float m_fPressure;
    std::vector<IObserver*> m_pObservers;
};

CurrentConditionsDisplay 类,显示当前天气的公告板

class CurrentConditionsDisplay : public IObserver, public IDisplayElement {
public:
    CurrentConditionsDisplay(ISubject *weatherData) {
        m_fTemperature = 0.0;
        m_fHumidity = 0.0;
        m_fPressure = 0.0;
        m_pWeatherData = weatherData;
        m_pWeatherData->RegisterObserver(this); // 注册
    }
    void Display(void) {
        printf("Current conditions: %.1f*C %.1f%% %.1fhPa\n", m_fTemperature, m_fHumidity, m_fPressure);
    }
    void Update(void) {
        WeatherData *pTmp = dynamic_cast<WeatherData*>(m_pWeatherData);
        if(pTmp) {
            // 去向 WeatherData "拉"数据
            m_fTemperature = pTmp->GetTemperature();
            m_fHumidity = pTmp->GetHumidity();
            m_fPressure = pTmp->GetPressure();
            Display();
        }
    }
    void Remove(void) {
        m_pWeatherData->RemoveObserver(this); // 取消注册
    }
private:
    float m_fTemperature; // 温度
    float m_fHumidity;    // 湿度
    float m_fPressure;    // 气压
    ISubject *m_pWeatherData;
};

ForecastDisplay 类,显示未来几天温度的公告板

class ForecastDisplay : public IObserver, public IDisplayElement {
public:
    ForecastDisplay(ISubject *weatherData) {
        m_aForecastTemperatures.clear();
        m_pWeatherData = weatherData;
        m_pWeatherData->RegisterObserver(this); // 注册
    }
    void Display(void) {
        std::vector<float>::iterator it;
        int i = 0;
        for(it = m_aForecastTemperatures.begin(); it != m_aForecastTemperatures.end(); it++) {
            printf("The next %d days temperature: %.1f*C\n", ++i, *it);
        }
    }
    void Update(void) {
        WeatherData *pTmp = dynamic_cast<WeatherData*>(m_pWeatherData);
        if(pTmp) {
            // 去向 WeatherData "拉"数据
            pTmp->GetForecastTemperatures(m_aForecastTemperatures);
            Display();
        }
    }
    void Remove(void) {
        m_pWeatherData->RemoveObserver(this); // 取消注册
    }
private:
    std::vector<float> m_aForecastTemperatures; // 未来几天的温度
    ISubject *m_pWeatherData;
};

到这里,整个气象站的应用就改造完成了
两个公告板 CurrentConditionsDisplay 和 ForecastDisplay 均实现了 IObserver 和 IDisplayElement 接口,在它们的构造方法中会调用 WeatherData 的 RegisterObserver 方法将自己注册成观察者,这样 WeatherData 就会持有观察者对象们的指针或引用,并将它们保存到一个集合中。当 WeatherData 状态发送变化时就会遍历这个集合,循环调用观察者公告板的 Update() 方法
观察者模式将观察者和主题(被观察者)彻底解耦,主题只知道观察者实现了某一接口(也就是 IObserver 接口)。并不需要知道观察者的具体类是谁、做了些什么或者其它任何细节
任何时候我们都可以增加新的观察者。因为主题唯一依赖的东西是一个实现了 IObserver 接口的对象列表

再看看 main 函数

int main(int argc, char *argv[]) {
    WeatherData *pWeatherData = new WeatherData();
    CurrentConditionsDisplay *pCurrentDisplay = new CurrentConditionsDisplay(pWeatherData);
    ForecastDisplay *pForecastDisplay = new ForecastDisplay(pWeatherData);
    pWeatherData->MeasurementsChanged();
    pCurrentDisplay->Remove();
    pForecastDisplay->Remove();
    // 取消注册之后,主题状态再有改变的话,观察者们也接收不到通知了
    pWeatherData->MeasurementsChanged(); 
    delete pWeatherData;
    delete pCurrentDisplay;
    delete pForecastDisplay;
    return 0;
}

运行结果,还算顺利

Current conditions: 16.0*C 24.0% 806.0hPa
The next 1 days temperature: 75.0*C
The next 2 days temperature: 19.0*C
The next 3 days temperature: 77.0*C
The next 4 days temperature: 83.0*C
The next 5 days temperature: 24.0*C

慢着慢着,你在 NotifyObservers() 里面遍历所有观察者去调用其 Update() 方法,万一某个观察者的 Update() 方法很耗时,另外的观察者岂不是很久才会被通知到?
在这里插入图片描述

有道理,我们可以采用异步的方式来解决这个问题

Version 2.1

异步调用是为了解决同步调用存在阻塞情况而产生的一种调用方式。在 NotifyObservers() 里每次都通过创建新线程的方式来调用观察者们的 Update() 方法,NotifyObservers() 的代码接着继续往下执行,这样就不会因为某个观察者的 Update() 方法太耗时而导致阻塞到其它观察者接收通知。举例代码如下

class WeatherData : public ISubject {
public:
    ......
    void NotifyObservers(void) {
        std::vector<IObserver*>::iterator it;
        for(it = m_pObservers.begin(); it != m_pObservers.end(); it++) {
            pthread_t threadId;
            pthread_create(&threadId, NULL, ThreadRun, *it); // 创建线程
        }
    }
    static void *ThreadRun(void *data) {
        if(data) {
            IObserver *observer = (IObserver *)data;
            observer->Update();
            pthread_exit(NULL);
        }
        return NULL;
    }
    ......
};

main 函数需要加个延时等待子线程执行完成再退出

int main(int argc, char *argv[]) {
    ......
    pWeatherData->MeasurementsChanged();
    // 主线程等待其它线程结束
    sleep(1);
    delete pWeatherData;
    ......
}

观察者模式定义

现在,我们来说下什么是观察者模式?
观察者模式,属于行为型模式的一种,它定义了对象之间的一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使观察者对象们能够自动更新自己

观察者模式和发布订阅模式

在观察者模式中,有两个角色,一个是 Subject (主题),用来维护一个 Observer 列表,另一个角色就是 Observer (观察者),在 Observer 中定义了一个具体的 Update 方法,用来执行相关的操作。
整个过程就是当 Subject 的某个值发生变化后,Subject 调用 Notify 方法(实际就是循环调用 Observer 列表中每个 Observer 的 Update 方法,并把新的值作为 Update 的参数传递进去)。
从中我们可以看出在 Subject 中直接调用了 Observer 中的方法,也就是说 Subject 和 Observer 的联系实际上是非常紧密的。

举个例子,现在有一个房东他要出租房子,当有空房子的时候,他就会去通知曾经来询问过的租客,那么这个时候房东就是直接知道租客的电话和需求(要住什么样的房子)的,也就是说房东和租客之间实际上是存在联系的。大致的流向图如下
在这里插入图片描述

前面说到 Subject 和 Observer 联系是非常紧密的,因为要在 Subject 中调用 Observer 中的方法。
那么发布订阅模式就可以解耦合,把调用的任务交给一个调度中心(中介),让调度中心去通知各个订阅者。

接着上面的例子。房东有钱后,自己变懒了,他不想每次有房源后,自己还要亲自打电话通知之前预留电话想要租房的租客,因为他不想记住那些租客的电话和需求(有钱了就不想干这些活,他要躺着赚钱)。
于是他就找到了中介,每次空出房子后,直接告诉中介我这里有什么样的房子,中介这里记录着哪些租客有着什么样的需求,中介再去联系有这样需求的租客。
那么这里的房东和租客之间是没有联系的,房东从此不用再亲自打电话去通知每一个有着这样需求的租客,只需要告诉中介一个人就行,剩下的事情中介去通知。那么整个过程就如下面这样
在这里插入图片描述

观察者模式和发布订阅模式的区别应该就是当有房源消息的时候,到底是谁来通知租客,观察者模式是房东自己本人去通知,而发布订阅模式则是中介去通知

观察者模式的优缺点

无论哪种模式都有其优缺点,当然我们每次在编写代码的时候需要考虑下其利弊
观察者模式的优点:

  1. 可以让表示层和数据逻辑层分离,并在主题和观察者之间建立一个抽象的耦合 (低耦合,主题只知道观察者实现了 Observer 接口)
  2. 建立了一套触发机制,支持广播式通信

观察者模式的缺点:

  1. 如果一个主题对象有很多观察者的话,将所有的观察者都通知到会花费很多时间
  2. 如果观察者和主题对象有循环依赖的话,主题对象会触发它们之间进行循环调用,可能导致系统崩溃
  3. 没有相应的机制让观察者知道主题对象是怎么发生变化的,而仅仅只是知道主题对象发生了变化

总结

应用场景

当一个对象某些状态的改变需要同时通知其它对象时,并且它不知道具体有多少对象有待改变的时候,应该考虑使用观察者模式,进行广播式通知

注意事项

  1. 避免循环依赖
  2. 如果顺序执行,某一观察者错误会导致系统卡壳,则推荐采用异步方式(如我们的 Version2.1)
  3. 上面 Version1.0 的例子,数据是 WeatherData 推过来的;Version2.0 的例子,数据则是观察者主动去获取的
    推模型:主题对象向观察者推送主题的详细信息,不管观察者是否需要
    拉模型:主题对象在通知观察者的时候,只传递少量信息。如果观察者需要更具体的信息,由观察者主动到主题对象中获取

有了观察者模式,你将会消息灵通!
在这里插入图片描述

参考资料

https://www.jianshu.com/p/9f2c8ae57cac

https://www.cnblogs.com/adamjwh/p/10913660.html

https://www.jianshu.com/p/d55ee6e83d66

https://baijiahao.baidu.com/s?id=1639044219412817957&wfr=spider&for=pc

Head+First设计模式(中文版).pdf

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cfl927096306

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值