[关闭]
@Dmaxiya 2020-12-28T22:56:23.000000Z 字数 18323 阅读 913

观察者模式

博文


定义

“观察者模式”是一个坑很多的模式。

要实现一个简单的观察者模式可能并不困难,但如果想要实现好一个没有 bug、功能完善的观察者模式,或许也不容易,这其中有许多值得注意的地方。

本文将通过一个布告板的例子[1],首先用最直观的方式实现,分析代码实现中可能存在的问题,引出并应用观察者模式,以达到“优美”地解决案例的目的,在读者对观察者模式有一定了解之后,提出关于观察者模式的一些小知识点,以及不同场景下应用该模式需要注意的地方,最终介绍观察者模式的实际应用。

案例

现在已经有一个能获取实际气象数据(温度、湿度、空气质量指数 AQI)的气象站以及三个布告板,需要实现一个功能,当气象数据有变化时:

  1. 能在“当前状态”布告板上刷新当前显示的温度、湿度
  2. 能在“统计”布告板上刷新今日的最高温度和最低温度
  3. 能在“AQI”布告板上刷新 AQI 指数及空气质量等级

类图

思路

  1. 定义三个布告板类:当前状态布告板 currentConditionDisplayBoard,统计布告板 statisticsDisplayBoard 以及 AQI 布告板 aqiDisplayBoard。其中当前状态布告板与 AQI 布告板每当其 update 方法被调用,就展示传入的最新数据,统计布告板需要储存历史的最高、最低温度,若最新温度不在该范围内,则不刷新展示;
  2. 定义一个 weatherData 类,该类用于储存上一次观测到的温度 temperature、湿度 humidity 以及空气质量指数 aqi,以调用 measurementChange 方法来模拟气象站“监测”到了数值的改变,方法的入参为三个指针,只有当某项数值有改变时,才会传入有效的地址,否则只会传入 NULLmeasurementChange 方法内部根据不同的值被更新,将调用不同 DisplayBoardupdate 方法,更新字段及布告板对应关系如下表:
更新字段 布告板
temperature currentConditionDisplayBoard / statisticsDisplayBoard
humidity currentConditionDisplayBoard
aqi aqiDisplayBoard

代码1

详细代码见:observer_mode_sample/sample1

current_condition_display_board

  1. class currentConditionDisplayBoard {
  2. public:
  3. void update(double temperature, double humidity) {
  4. printf("=========== Current Condition Display Board ============\n");
  5. // output update data
  6. }
  7. };

statistics_display_board

  1. class statisticsDisplayBoard {
  2. private:
  3. double minTemperature;
  4. double maxTemperature;
  5. public:
  6. statisticsDisplayBoard() {
  7. minTemperature = std::numeric_limits<double>::infinity();
  8. maxTemperature = -minTemperature;
  9. }
  10. void update(double temperature) {
  11. if (temperature >= minTemperature && temperature <= maxTemperature) {
  12. return ;
  13. }
  14. maxTemperature = std::max(maxTemperature, temperature);
  15. minTemperature = std::min(minTemperature, temperature);
  16. printf("=============== Statistics Display Board ===============\n");
  17. // output update data
  18. }
  19. };

aqi_display_board

  1. class aqiDisplayBoard {
  2. public:
  3. void update(double aqi) {
  4. printf("================== AQI Display Board ===================\n");
  5. // output update data
  6. }
  7. };

weather_data

  1. class weatherData {
  2. private:
  3. int updateTimes;
  4. double temperature;
  5. double humidity;
  6. double aqi;
  7. currentConditionDisplayBoard &ccBoard;
  8. statisticsDisplayBoard &sBoard;
  9. aqiDisplayBoard &aqiBoard;
  10. public:
  11. weatherData(currentConditionDisplayBoard &ccBoard, statisticsDisplayBoard &sBoard, aqiDisplayBoard &aqiBoard):
  12. ccBoard(ccBoard), sBoard(sBoard), aqiBoard(aqiBoard) {
  13. // Initialize other member variables to 0
  14. }
  15. void mesurementChange(double *temperature, double *humidity, double *aqi) {
  16. if (temperature != NULL) {
  17. this->temperature = *temperature;
  18. }
  19. if (humidity != NULL) {
  20. this->humidity = *humidity;
  21. }
  22. if (aqi != NULL) {
  23. this->aqi = *aqi;
  24. }
  25. ++updateTimes;
  26. printf("Update case %d:\n", updateTimes);
  27. if (temperature != NULL || humidity != NULL) {
  28. ccBoard.update(this->temperature, this->humidity);
  29. }
  30. if (temperature != NULL) {
  31. sBoard.update(this->temperature);
  32. }
  33. if (aqi != NULL) {
  34. aqiBoard.update(this->aqi);
  35. }
  36. }
  37. };

main

  1. int main() {
  2. double temperature;
  3. double humidity;
  4. double aqi;
  5. currentConditionDisplayBoard ccBoard;
  6. statisticsDisplayBoard sBoard;
  7. aqiDisplayBoard aqiBoard;
  8. weatherData wd(ccBoard, sBoard, aqiBoard);
  9. // ccBoard 与 sBoard 都会更新,aqiBoard 不会更新
  10. temperature = 25;
  11. humidity = 0.9;
  12. wd.mesurementChange(&temperature, &humidity, NULL);
  13. temperature = 26;
  14. wd.mesurementChange(&temperature, NULL, NULL);
  15. // ccBoard 会更新,sBoard 和 aqiBoard 都不会更新
  16. temperature = 25.5;
  17. wd.mesurementChange(&temperature, NULL, NULL);
  18. // ccBoard 和 sBoard 都不会更新,aqiBoard 会更新
  19. aqi = 50;
  20. wd.mesurementChange(NULL, NULL, &aqi);
  21. return 0;
  22. }

运行结果:

  1. Update case 1:
  2. =========== Current Condition Display Board ============
  3. temperature: 25.0 humidity: 0.9
  4. =============== Statistics Display Board ===============
  5. max temperature: 25.0 min temperature: 25.0
  6. Update case 2:
  7. =========== Current Condition Display Board ============
  8. temperature: 26.0 humidity: 0.9
  9. =============== Statistics Display Board ===============
  10. max temperature: 26.0 min temperature: 25.0
  11. Update case 3:
  12. =========== Current Condition Display Board ============
  13. temperature: 25.5 humidity: 0.9
  14. Update case 4:
  15. ================== AQI Display Board ===================
  16. aqi: 50.0 level: 1

代码分析

查看上面的代码可以发现,weatherData 类持有了三个布告板的引用,并且需要在初始化时将这三个对象传入,每当有数据变更时,都需要调用每个一布告板的 update 方法,相比于 weatherData 类,布告板的展示内容、它们所关心的变更数据内容的变化是更频繁的:

  1. 统计布告板后续可能需要展示近 7 天的 AQI 数据变化,那么 weatherData 类就需要在代码中添加 “AQI 值变更时调用 statisticsDisplayBoard 类的 update 方法”的逻辑,此时 update 方法的入参也需要修改;
  2. 后面可能新增各种各样的布告板,以展示不同的信息,此时 mesurementChange 方法的代码量将随着需要 update 布告板数量的增加而增加

相对而言,weatherData 类监测的数据类型变化的频率会更低一些,低频变动的模块依赖于高频变动模块的具体实现,将导致低频变动模块代码的频繁改动,大大提高了代码的开发维护成本,这违反了依赖倒置原则开放封闭原则

同时,由于 weatherData 类需要更新哪几个布告板是硬编码的,这就导致了无法在程序运行过程中进行动态地绑定、解绑这种 update 的调用关系,

观察者模式

定义

观察者模式定义了对象之间的一对多依赖,当一个对象改变状态时,它的所有依赖者都会收到通知并更新。

类图

参与成员

说明

目标对象(被观察者)对外提供注册观察者的 attach 方法,以及取消注册的 detach 方法,所有注册的观察者都储存于 os 数据成员中,observer 是一个抽象类,只要是继承了 observer 并实现其 update 方法的类型就可以作为 subject 的观察者。

每当目标对象的状态发生改变时,就会通过 notify 方法通知所有 observer,调用它们的 update 方法,而每个 observer 在接收到通知后,执行相应的逻辑,其中可能需要获取目标对象的状态,concreteSubject 提供了 getState 方法让收到通知的观察者获取到它的最新状态,因此在 concreteObserver 下持有目标对象的引用。

适用性

应用于案例

类图

思路

weatherData 即被观察者,三个布告板为观察者,每当 weatherData 监测到数值发生变化时,就直接向所有的 observer 发送通知,它不需要关心哪些观察者对哪些数据的变更感兴趣。

当观察者收到通知时,再向 weatherData 对象获取自己所感兴趣的数据,通过与上一次储存的目标对象状态相比较,来判断对应状态是否发生了改变,若发生改变,则刷新布告板,因此三个布告板类型都持有 weatherData 的引用以及需要关心的目标状态。

代码2

详细代码见:observer_mode_sample/sample2

observer

  1. class observer {
  2. public:
  3. virtual void update() = 0;
  4. };

current_condition_display_board

  1. class currentConditionDisplayBoard: public observer {
  2. private:
  3. double temperature;
  4. double humidity;
  5. weatherData &wd;
  6. public:
  7. currentConditionDisplayBoard(weatherData &wd): wd(wd) { /* Initialize other member variables to 0 */ }
  8. void update() {
  9. double temperature = wd.getTemperature();
  10. double humidity = wd.getHumidity();
  11. if (util::equal(temperature, this->temperature) && util::equal(humidity, this->humidity)) {
  12. return ;
  13. }
  14. this->temperature = temperature;
  15. this->humidity = humidity;
  16. printf("=========== Current Condition Display Board ============\n");
  17. // output udpate data
  18. }
  19. };

statistics_display_board

  1. class statisticsDisplayBoard: public observer {
  2. private:
  3. double minTemperature;
  4. double maxTemperature;
  5. weatherData &wd;
  6. public:
  7. statisticsDisplayBoard(weatherData &wd): wd(wd) {
  8. minTemperature = std::numeric_limits<double>::infinity();
  9. maxTemperature = -minTemperature;
  10. }
  11. void update() {
  12. double temperature = wd.getTemperature();
  13. if (temperature >= minTemperature && temperature <= maxTemperature) {
  14. return ;
  15. }
  16. maxTemperature = std::max(maxTemperature, temperature);
  17. minTemperature = std::min(minTemperature, temperature);
  18. printf("=============== Statistics Display Board ===============\n");
  19. // output update data
  20. }
  21. };

aqi_display_board

  1. class aqiDisplayBoard: public observer {
  2. private:
  3. double aqi;
  4. weatherData &wd;
  5. public:
  6. aqiDisplayBoard(weatherData &wd): wd(wd) { /* Initialize other member variables to 0 */ }
  7. void update() {
  8. double aqi = wd.getAQI();
  9. if (util::equal(aqi, this->aqi)) {
  10. return ;
  11. }
  12. this->aqi = aqi;
  13. printf("================== AQI Display Board ===================\n");
  14. // output update data
  15. }
  16. };

subject

  1. class subject {
  2. private:
  3. std::vector<observer*> os;
  4. public:
  5. void attach(observer *o) {
  6. os.push_back(o);
  7. }
  8. void detach(observer *o) {
  9. os.erase(std::remove(os.begin(), os.end(), o), os.end());
  10. }
  11. void notify() {
  12. for (auto o: os) {
  13. o->update();
  14. }
  15. }
  16. };

weather_data

  1. class weatherData: public subject {
  2. private:
  3. int updateTimes;
  4. double temperature;
  5. double humidity;
  6. double aqi;
  7. public:
  8. weatherData() { /* Initialize member variables to 0 */ }
  9. double getTemperature() { return temperature; }
  10. double getHumidity() { return humidity; }
  11. double getAQI() { return aqi; }
  12. void mesurementChange(double *temperature, double *humidity, double *aqi) {
  13. if (temperature != NULL) {
  14. this->temperature = *temperature;
  15. }
  16. if (humidity != NULL) {
  17. this->humidity = *humidity;
  18. }
  19. if (aqi != NULL) {
  20. this->aqi = *aqi;
  21. }
  22. ++updateTimes;
  23. printf("Update case %d:\n", updateTimes);
  24. notify();
  25. }
  26. };

main

  1. int main() {
  2. double temperature;
  3. double humidity;
  4. double aqi;
  5. weatherData wd;
  6. currentConditionDisplayBoard ccBoard(wd);
  7. statisticsDisplayBoard sBoard(wd);
  8. aqiDisplayBoard aqiBoard(wd);
  9. wd.attach(&ccBoard);
  10. wd.attach(&sBoard);
  11. wd.attach(&aqiBoard);
  12. // ccBoard 与 sBoard 都会更新,aqiBoard 不会更新
  13. temperature = 25;
  14. humidity = 0.9;
  15. wd.mesurementChange(&temperature, &humidity, NULL);
  16. temperature = 26;
  17. wd.mesurementChange(&temperature, NULL, NULL);
  18. // ccBoard 会更新,sBoard 和 aqiBoard 都不会更新
  19. temperature = 25.5;
  20. wd.mesurementChange(&temperature, NULL, NULL);
  21. // ccBoard 和 sBoard 都不会更新,aqiBoard 会更新
  22. aqi = 50;
  23. wd.mesurementChange(NULL, NULL, &aqi);
  24. // aqiBoard 取消订阅,关于 aqi 的更新将不会触发 aqiBoard 的刷新显示
  25. wd.detach(&aqiBoard);
  26. aqi = 300;
  27. wd.mesurementChange(NULL, NULL, &aqi);
  28. return 0;
  29. }

运行结果:

  1. Update case 1:
  2. =========== Current Condition Display Board ============
  3. temperature: 25.0 humidity: 0.9
  4. =============== Statistics Display Board ===============
  5. max temperature: 25.0 min temperature: 25.0
  6. Update case 2:
  7. =========== Current Condition Display Board ============
  8. temperature: 26.0 humidity: 0.9
  9. =============== Statistics Display Board ===============
  10. max temperature: 26.0 min temperature: 25.0
  11. Update case 3:
  12. =========== Current Condition Display Board ============
  13. temperature: 25.5 humidity: 0.9
  14. Update case 4:
  15. ================== AQI Display Board ===================
  16. aqi: 50.0 level: 1
  17. Update case 5:

代码分析

从代码中可以看出,后续若有新布告板需要监听 weatherData 中数据的变更,只要新增一个布告板类,并继承 observer 抽象类即可,即使原布告板需要关注新的变更字段,也无需修改 weatherData 类中的任何一行代码,只需要修改对应布告板中的代码。达到了面向扩展开放,面向修改封闭的目的。

weatherData 类只依赖 observer 抽象类,而不依赖观察者类的具体实现,观察者与目标对象其中一方的改变并不会影响另一方,实现了松耦合

main 函数中可以看到,由于 subject 提供了 attachdetach 方法,这使得我们可以在运行时动态地添加、删除观察者,不因将观察逻辑写死在代码中而限制这种绑定关系。

补充说明

util::equal 方法是 util 类的静态成员函数,用于比较两个 double 类型的变量是否相等,由于浮点数在计算过程中容易损失精度,因此在 equal 方法中认为待比较的两个浮点数误差在 范围内即相等。

上面的代码中,并没有对 observer 做重复注册的检查,如果同一个 observer 进行了多次注册,我们认为观察者想要在目标对象发生一次变更时收到多条通知;若由于特殊原因,观察者不得不进行多次注册,而最终只期望在一次变更时收到一次通知,则目标对象可以在每次变更发生时生成一个唯一 id,由观察者来进行幂等性的逻辑保证。

Bug

上一段代码已经实现了观察者模式,但其中存在一个 bug,如果▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ [2](bug 描述被 ▇▇ 隐藏,建议思考之后,再跳转至注脚查看),为了解决这个 bug,下面提供了两种方案(当然最简单的方案是令布告板类中的 observerState 类型为指针,初始值为 NULL 表示未更新过,不过为了介绍关于观察者模式的更多实现,请读者耐心往下看~)。

方案一

类图

思路

布告板在调用 attach 注册时,显式地指定其需要关注的“方面”(aspect,如温度、湿度),当 weatherData 监测到数据发生变更时,提取出变更所对应的 aspect,只向关注这些“方面”的布告板发送变更通知。注册后布告板与 aspect 的对应关系记录在一个 observerWithAspect 类型的对象中,通过查找变更的 aspect 是否存在于 unordered_set 中,来判断是否向对应布告板发送通知。

当布告板接收到通知时,可以知道自己关注的部分(温度、湿度、AQI)一定发生了变更,它只要负责更新(刷新布告板)即可,不需要做新、旧状态的比较才能得出 weatherData 是否发生了变更的结论。可以发现,在 AQI 与当前状态布告板类型中,已经不再储存 subjectState 相关的字段,而统计布告板由于始终需要记录历史最高 / 最低温度,因此仍然不能取消 maxTemperatureminTemperature 成员变量。

代码3

详细代码见:observer_mode_sample/sample3

subject

  1. typedef int aspect;
  2. class subject {
  3. private:
  4. class observerWithAspect {
  5. private:
  6. observer *o;
  7. std::unordered_set<aspect> as;
  8. public:
  9. observerWithAspect(observer *o, const std::initializer_list<aspect> &al): o(o) {
  10. for (auto a: al) {
  11. this->as.insert(a);
  12. }
  13. }
  14. friend class subject;
  15. };
  16. std::vector<observerWithAspect*> owas;
  17. static const int npos = -1;
  18. public:
  19. ~subject() {
  20. for (auto owa: owas) {
  21. delete owa;
  22. }
  23. }
  24. void attach(observer *o, std::initializer_list<aspect> al) {
  25. observerWithAspect *owa = new observerWithAspect(o, al);
  26. owas.push_back(owa);
  27. }
  28. void detach(observer *o, std::initializer_list<aspect> al) {
  29. int pos = indexOfObserver(o);
  30. if (pos == npos) {
  31. return ;
  32. }
  33. for (auto a: al) {
  34. owas[pos]->as.erase(a);
  35. }
  36. if (owas[pos]->as.empty()) {
  37. delete owas[pos];
  38. owas.erase(owas.begin() + pos);
  39. }
  40. }
  41. void notify(const std::vector<aspect> &as) {
  42. for (auto owa: owas) {
  43. for (auto a: as) {
  44. if (owa->as.find(a) != owa->as.end()) {
  45. owa->o->update();
  46. break;
  47. }
  48. }
  49. }
  50. }
  51. };

weather_data

  1. class weatherData: public subject {
  2. private:
  3. int updateTimes;
  4. double temperature;
  5. double humidity;
  6. double aqi;
  7. public:
  8. static const aspect aspectTemperature;
  9. static const aspect aspectHumidity;
  10. static const aspect aspectAQI;
  11. weatherData() { /* Initialize member variables to 0 */ }
  12. void mesurementChange(double *temperature, double *humidity, double *aqi) {
  13. std::vector<aspect> as;
  14. if (temperature != NULL) {
  15. this->temperature = *temperature;
  16. as.push_back(this->aspectTemperature);
  17. }
  18. if (humidity != NULL) {
  19. this->humidity = *humidity;
  20. as.push_back(aspectHumidity);
  21. }
  22. if (aqi != NULL) {
  23. this->aqi = *aqi;
  24. as.push_back(aspectAQI);
  25. }
  26. ++updateTimes;
  27. printf("Update case %d:\n", updateTimes);
  28. notify(as);
  29. }
  30. };
  31. const aspect weatherData::aspectTemperature = 0;
  32. const aspect weatherData::aspectHumidity = 1;
  33. const aspect weatherData::aspectAQI= 2;

main

  1. int main() {
  2. double temperature;
  3. double humidity;
  4. double aqi;
  5. weatherData wd;
  6. currentConditionDisplayBoard ccBoard(wd);
  7. statisticsDisplayBoard sBoard(wd);
  8. aqiDisplayBoard aqiBoard(wd);
  9. wd.attach(&ccBoard, {wd.aspectTemperature, wd.aspectHumidity});
  10. wd.attach(&sBoard, {wd.aspectTemperature});
  11. wd.attach(&aqiBoard, {wd.aspectAQI});
  12. // bug fix:现在可以触发所有更新了
  13. temperature = 0;
  14. humidity = 0;
  15. aqi = 0;
  16. wd.mesurementChange(&temperature, &humidity, &aqi);
  17. // ccdBoard 和 sdBoard 都会更新,aqiBoard 不会更新
  18. temperature = 25;
  19. humidity = 0.9;
  20. wd.mesurementChange(&temperature, &humidity, NULL);
  21. temperature = 26;
  22. wd.mesurementChange(&temperature, NULL, NULL);
  23. // ccdBoard 会更新,sdBoard 和 aqiBoard 都不会更新
  24. temperature = 25.5;
  25. wd.mesurementChange(&temperature, NULL, NULL);
  26. // ccdBoard 和 sdBoard 都不会更新,aqiBoard 会更新
  27. aqi = 50;
  28. wd.mesurementChange(NULL, NULL, &aqi);
  29. // aqiBoard 取消订阅,关于 aqi 的更新将不会触发 aqiBoard 的刷新显示
  30. wd.detach(&aqiBoard, {wd.aspectAQI});
  31. aqi = 300;
  32. wd.mesurementChange(NULL, NULL, &aqi);
  33. return 0;
  34. }

补充说明

  1. 以上只展示了相比于原观察者模式实现中有明显差异的代码,关于 observer 及三个布告板的实现尽管有些许变动,但读者可以通过上面的类图了解其实现,完整代码请移步:observer_mode_sample/sample3
  2. initializer_listc++11 提供的新特性,在这里用于实现可变个数参数的传参,在调用时可以传入任意数量的 aspect,实际上可以用 vector 代替;
  3. subjectobserverWithAspect 作为其私有内部类,原因是 observerWithAspect 类只用于拼装 observeraspect,不应该被除 subject 以外的其他类型所调用。

方案二

类图

思路

每当 weatherData 类监测到数据变更时,将全量的变更消息通过 notify 中的 void* 参数推送出去,由布告板通过接收到的消息来判断自己所关心的变化是否发生了,如果发生,则刷新布告板的内容。

c++ 中,void* 可以与任意类型的指针互相转换,这样就可以将变更信息通过一个结构体指针传出,当然 void* 转换前后必须是相同的结构体指针类型,否则程序可能出现严重错误。也可以将 void* 改为 char 数组,使用序列化、反序列化的方式来进行变更信息的转化,所有接收通知的布告板都需要遵守 weatherData 定下的变更通知信息的转化规则,才能收到正确的信息。

注意到这里已经不存在布告板到 weatherData 实例的引用,weatherData 类也不再对外提供 getState 方法,因为在布告板收到通知时,已经知道了所有的变更信息,不需要再通过 weatherData 的引用来获取它的最新状态了。

代码4

详细代码见:observer_mode_sample/sample4

observer

  1. class observer {
  2. public:
  3. virtual void update(const void* changeInfo) = 0;
  4. };

aqi_display_board

  1. class aqiDisplayBoard: public observer {
  2. private:
  3. double aqi;
  4. bool getInfo(const void *changeInfo) {
  5. const weatherData::changeInfo *ci = reinterpret_cast<const weatherData::changeInfo*>(changeInfo);
  6. if (ci->aqi == NULL) {
  7. return true;
  8. }
  9. aqi = *(ci->aqi);
  10. return false;
  11. }
  12. public:
  13. aqiDisplayBoard() { /* Initialize member variables to 0 */ }
  14. void update(const void *changeInfo) {
  15. bool skip = getInfo(changeInfo);
  16. if (skip) {
  17. return ;
  18. }
  19. printf("================== AQI Display Board ===================\n");
  20. printf("\taqi: %.1f\t\tlevel: %d\n\n", aqi, getLevel());
  21. }
  22. };

subject

  1. class subject {
  2. private:
  3. std::vector<observer*> os;
  4. public:
  5. void notify(const void *changeInfo) {
  6. for (auto o: os) {
  7. o->update(changeInfo);
  8. }
  9. }
  10. };

weather_data

  1. class weatherData: public subject {
  2. private:
  3. int updateTimes;
  4. double temperature;
  5. double humidity;
  6. double aqi;
  7. public:
  8. struct changeInfo {
  9. double *temperature;
  10. double *humidity;
  11. double *aqi;
  12. changeInfo(double *temperature, double *humidity, double *aqi):
  13. temperature(temperature), humidity(humidity), aqi(aqi) {}
  14. };
  15. weatherData() { /* Initialize member variables to 0 */ }
  16. void mesurementChange(double *temperature, double *humidity, double *aqi) {
  17. changeInfo *ci = new changeInfo(temperature, humidity, aqi);
  18. if (temperature != NULL) {
  19. this->temperature = *temperature;
  20. }
  21. if (humidity != NULL) {
  22. this->humidity = *humidity;
  23. }
  24. if (aqi != NULL) {
  25. this->aqi = *aqi;
  26. }
  27. ++updateTimes;
  28. printf("Update case %d:\n", updateTimes);
  29. notify(ci);
  30. }
  31. };

补充说明

  1. 以上只展示部分代码,由于另外两种布告板的实现与 AQI 布告板实现相似,subjectattachdetach 方法的实现与应用于案例中的实现完全相同,故没有展示,完整代码请移步:observer_mode_sample/sample4
  2. 携带变更信息的 changeInfo 使用 const void* 类型,目的是防止在多个 update 调用过程中,changeInfo 中的值被修改,造成严重影响;
  3. 由于 changeInfo 只储存关于 weatherData 的变更信息,且应该允许外部访问到 changeInfo 中的数据成员,因此将 changeInfo 作为 weatherDatapublic 内部结构体。

关于

推拉模型

不论是推模型还是拉模型,都要注意的是:不要设计特定于某个观察者的更新协议,因为我们无法预料后续可能会出现的其他观察者。

触发 notify 的时机

  1. 在每次目标对象的状态发生改变时,就自己在内部调用 notify 方法通知被观察者

    • 优点:目标的调用者无需在改变目标状态后,调用 notify 进行通知(不会忘记)

    • 缺点:多次对同一个目标的更新可能发送多条更新信息,降低效率

  2. notify 方法的调用交给目标的调用者,由调用者来确定在“适当的时机”进行调用

    • 优点:这样可以在一系列状态改变完毕后,一次性触发 notify,避免不必要的通知
    • 缺点:notify 的触发容易被调用者忘记

复杂的依赖

可能存在一个观察者观察多个被观察者的情况,例如一个表格依赖多个数据源,或者是游戏中的成就系统,甚至可能存在多对多的情况(例如用户使用界面按钮点击之间的互斥关系,点击了某几个按钮,则与其互斥状态的按钮需要 disable)。当观察者和被观察者之间的依赖关系变得更复杂的时候,需要引入一个 ChangeManager 管理这种依赖关系,目前有两种 ChangeManager:

dagChangeManager 是 Mediator(中介者)模式的一个实例,通常情况下只需要一个 ChangeManager 且全局可见,此时也可以使用 Singleton(单例)模式。

注意

意外的更新

如果被观察者允许某些观察者更新自身的状态,由于每个观察者都并不知道其他观察者的存在,因此它对改变被观察者所产生的影响与代价一无所知。如果依赖关系的定义或维护不当,可能造成错误的更新,而这种错误通常很难被捕捉到。

状态一致

在发送通知之前需要确保被观察者的状态自身是一致的,因为在发出通知之后,观察者需要查询目标的状态,若此时自身状态不一致,则容易出现逻辑错误,下面这段代码就非常容易在无意中犯下这样的错误:

  1. class baseSubject {
  2. protected:
  3. int value;
  4. public:
  5. void notify() {
  6. printf("notify now!\nvalue: %d\n", value);
  7. }
  8. void operation(int newValue) {
  9. value = newValue;
  10. notify();
  11. }
  12. };
  13. class subject: public baseSubject {
  14. public:
  15. void operation(int newValue) {
  16. baseSubject::operation(newValue);
  17. // 通知已发送完毕
  18. value += newValue;
  19. }
  20. };
  21. /**
  22. * 预期执行结果:
  23. * notify now!
  24. * value: 4
  25. * 实际执行结果:
  26. * notify now!
  27. * value: 2
  28. */
  29. int main() {
  30. subject s;
  31. s.operation(2);
  32. return 0;
  33. }

使用模板方法模式可以保证 notify 函数在所有状态确定之后才被调用:

  1. class baseSubject {
  2. protected:
  3. int value;
  4. public:
  5. void notify() {
  6. printf("notify now!\nvalue: %d\n", value);
  7. }
  8. virtual void doOperation(int newValue) {
  9. value = newValue;
  10. }
  11. void operation(int newValue) {
  12. doOperation(newValue);
  13. notify();
  14. }
  15. };
  16. class subject: public baseSubject {
  17. public:
  18. void doOperation(int newValue) {
  19. baseSubject::doOperation(newValue);
  20. value += newValue;
  21. }
  22. };
  23. int main() {
  24. subject s;
  25. s.operation(2);
  26. return 0;
  27. }

同步通知

观察者模式中的通知是同步进行的,只有所有观察者的操作都执行完毕之后,被观察者才会继续后面的操作,如果观察者进行的操作是比较耗时的,此时目标对象就会被观察者的流程阻塞,如果这种阻塞不是必须的,可以将观察者的操作推到另一个线程或工作队列中去。

在观察者模式中要十分小心其中混合的线程和锁,如果观察者试图获取目标的锁,这时就会产生死锁。在多线程引擎中,最好采取事件队列来做异步通信。

悬挂引用

sample2sample3 中,所有的观察者实例都持有一个 weatherData 的引用,而 sample4 中没有,sample2 与 3 中的 weatherData 作用是实现“拉模型”时需要通过该引用获取到目标状态,实际上可以将该引用放在 notify 中:

  1. class subject {
  2. public:
  3. void notify(const weatherData &wd) {
  4. for (auto o: os) {
  5. o->update(wd);
  6. }
  7. }
  8. };

观察者的 update 接口获取到该引用,就可以拿到目标状态。

实际上观察者保留指向被观察者的引用还有另一个更重要的作用:当观察者销毁时,它可以通过引用找到被观察者,取消自身在观察者的注册,如果被观察者的生命周期比观察者的周期更长,这是十分有必要的。

举个栗子,当玩家进入一个打怪副本时,会 new 出一个玩家的血条展示在界面的左上角,每当玩家受到攻击时,血条作为观察者就会收到一次攻击事件,并做出“扣血”的响应。当玩家退出副本后,副本中的血条要么被注销,要么仍然被目标对象(被观察者)引用,当玩家进入一个新副本时,每当他受到一次攻击,就会向上一个副本的血条发送一个攻击事件,而此时血条做出的任何更新响应都是无意义的。若观察者不是血条,而是受到攻击时播放声音的控件,此时玩家在受到攻击时就会听到双重攻击效果的声音,这种会产生副作用的通知,对玩家而言就是一个 bug。

同样,当被观察者的生命周期结束时,也应该向观察者发送通知,重置观察者到自身的引用,这可以通过向观察者发送一个“死亡”事件达到目的。

应用

参考

《游戏编程模式》

设计模式:可复用面向对象软件的基础

《head first 设计模式》

bilibili:23 个设计模式

代码仓库

  1. git clone https://github.com/Dmaxiya/observer_mode_sample

[1] 改编自《head first 设计模式》第二章观察者模式。
[2] 如果首次更新的 temperaturehumidity 值为 0,那么 ccBoardsBoard 将不会刷新
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注