@Dmaxiya
2020-12-28T22:56:23.000000Z
字数 18323
阅读 935
博文
“观察者模式”是一个坑很多的模式。
要实现一个简单的观察者模式可能并不困难,但如果想要实现好一个没有 bug、功能完善的观察者模式,或许也不容易,这其中有许多值得注意的地方。
本文将通过一个布告板的例子[1],首先用最直观的方式实现,分析代码实现中可能存在的问题,引出并应用观察者模式,以达到“优美”地解决案例的目的,在读者对观察者模式有一定了解之后,提出关于观察者模式的一些小知识点,以及不同场景下应用该模式需要注意的地方,最终介绍观察者模式的实际应用。
现在已经有一个能获取实际气象数据(温度、湿度、空气质量指数 AQI)的气象站以及三个布告板,需要实现一个功能,当气象数据有变化时:
currentConditionDisplayBoard
,统计布告板 statisticsDisplayBoard
以及 AQI 布告板 aqiDisplayBoard
。其中当前状态布告板与 AQI 布告板每当其 update
方法被调用,就展示传入的最新数据,统计布告板需要储存历史的最高、最低温度,若最新温度不在该范围内,则不刷新展示;weatherData
类,该类用于储存上一次观测到的温度 temperature
、湿度 humidity
以及空气质量指数 aqi
,以调用 measurementChange
方法来模拟气象站“监测”到了数值的改变,方法的入参为三个指针,只有当某项数值有改变时,才会传入有效的地址,否则只会传入 NULL
,measurementChange
方法内部根据不同的值被更新,将调用不同 DisplayBoard
的 update
方法,更新字段及布告板对应关系如下表:更新字段 | 布告板 |
---|---|
temperature | currentConditionDisplayBoard / statisticsDisplayBoard |
humidity | currentConditionDisplayBoard |
aqi | aqiDisplayBoard |
详细代码见:observer_mode_sample/sample1
current_condition_display_board
class currentConditionDisplayBoard {
public:
void update(double temperature, double humidity) {
printf("=========== Current Condition Display Board ============\n");
// output update data
}
};
statistics_display_board
class statisticsDisplayBoard {
private:
double minTemperature;
double maxTemperature;
public:
statisticsDisplayBoard() {
minTemperature = std::numeric_limits<double>::infinity();
maxTemperature = -minTemperature;
}
void update(double temperature) {
if (temperature >= minTemperature && temperature <= maxTemperature) {
return ;
}
maxTemperature = std::max(maxTemperature, temperature);
minTemperature = std::min(minTemperature, temperature);
printf("=============== Statistics Display Board ===============\n");
// output update data
}
};
aqi_display_board
class aqiDisplayBoard {
public:
void update(double aqi) {
printf("================== AQI Display Board ===================\n");
// output update data
}
};
weather_data
class weatherData {
private:
int updateTimes;
double temperature;
double humidity;
double aqi;
currentConditionDisplayBoard &ccBoard;
statisticsDisplayBoard &sBoard;
aqiDisplayBoard &aqiBoard;
public:
weatherData(currentConditionDisplayBoard &ccBoard, statisticsDisplayBoard &sBoard, aqiDisplayBoard &aqiBoard):
ccBoard(ccBoard), sBoard(sBoard), aqiBoard(aqiBoard) {
// Initialize other member variables to 0
}
void mesurementChange(double *temperature, double *humidity, double *aqi) {
if (temperature != NULL) {
this->temperature = *temperature;
}
if (humidity != NULL) {
this->humidity = *humidity;
}
if (aqi != NULL) {
this->aqi = *aqi;
}
++updateTimes;
printf("Update case %d:\n", updateTimes);
if (temperature != NULL || humidity != NULL) {
ccBoard.update(this->temperature, this->humidity);
}
if (temperature != NULL) {
sBoard.update(this->temperature);
}
if (aqi != NULL) {
aqiBoard.update(this->aqi);
}
}
};
main
int main() {
double temperature;
double humidity;
double aqi;
currentConditionDisplayBoard ccBoard;
statisticsDisplayBoard sBoard;
aqiDisplayBoard aqiBoard;
weatherData wd(ccBoard, sBoard, aqiBoard);
// ccBoard 与 sBoard 都会更新,aqiBoard 不会更新
temperature = 25;
humidity = 0.9;
wd.mesurementChange(&temperature, &humidity, NULL);
temperature = 26;
wd.mesurementChange(&temperature, NULL, NULL);
// ccBoard 会更新,sBoard 和 aqiBoard 都不会更新
temperature = 25.5;
wd.mesurementChange(&temperature, NULL, NULL);
// ccBoard 和 sBoard 都不会更新,aqiBoard 会更新
aqi = 50;
wd.mesurementChange(NULL, NULL, &aqi);
return 0;
}
运行结果:
Update case 1:
=========== Current Condition Display Board ============
temperature: 25.0 humidity: 0.9
=============== Statistics Display Board ===============
max temperature: 25.0 min temperature: 25.0
Update case 2:
=========== Current Condition Display Board ============
temperature: 26.0 humidity: 0.9
=============== Statistics Display Board ===============
max temperature: 26.0 min temperature: 25.0
Update case 3:
=========== Current Condition Display Board ============
temperature: 25.5 humidity: 0.9
Update case 4:
================== AQI Display Board ===================
aqi: 50.0 level: 1
查看上面的代码可以发现,weatherData
类持有了三个布告板的引用,并且需要在初始化时将这三个对象传入,每当有数据变更时,都需要调用每个一布告板的 update
方法,相比于 weatherData
类,布告板的展示内容、它们所关心的变更数据内容的变化是更频繁的:
weatherData
类就需要在代码中添加 “AQI 值变更时调用 statisticsDisplayBoard
类的 update
方法”的逻辑,此时 update
方法的入参也需要修改;mesurementChange
方法的代码量将随着需要 update
布告板数量的增加而增加相对而言,weatherData
类监测的数据类型变化的频率会更低一些,低频变动的模块依赖于高频变动模块的具体实现,将导致低频变动模块代码的频繁改动,大大提高了代码的开发维护成本,这违反了依赖倒置原则与开放封闭原则。
同时,由于 weatherData
类需要更新哪几个布告板是硬编码的,这就导致了无法在程序运行过程中进行动态地绑定、解绑这种 update
的调用关系,
观察者模式定义了对象之间的一对多依赖,当一个对象改变状态时,它的所有依赖者都会收到通知并更新。
subject
:被观察者,能知道它的所有观察者,提供注册、删除观察者对象的接口;observer
:观察者,为被观察者发生改变时需要获得通知的对象定义了更新接口的抽象类;concreteSubject
:具体的被观察者(目标对象),当其状态发生改变时,向各个观察者发送通知;concreteObserver
:具体的观察者,维护一个指向 concreteSubject
对象的引用,以及 subject
相关的状态 observerState
,这些状态应与被观察者状态保持一致。目标对象(被观察者)对外提供注册观察者的 attach
方法,以及取消注册的 detach
方法,所有注册的观察者都储存于 os
数据成员中,observer
是一个抽象类,只要是继承了 observer
并实现其 update
方法的类型就可以作为 subject
的观察者。
每当目标对象的状态发生改变时,就会通过 notify
方法通知所有 observer
,调用它们的 update
方法,而每个 observer
在接收到通知后,执行相应的逻辑,其中可能需要获取目标对象的状态,concreteSubject
提供了 getState
方法让收到通知的观察者获取到它的最新状态,因此在 concreteObserver
下持有目标对象的引用。
weatherData
即被观察者,三个布告板为观察者,每当 weatherData
监测到数值发生变化时,就直接向所有的 observer
发送通知,它不需要关心哪些观察者对哪些数据的变更感兴趣。
当观察者收到通知时,再向 weatherData
对象获取自己所感兴趣的数据,通过与上一次储存的目标对象状态相比较,来判断对应状态是否发生了改变,若发生改变,则刷新布告板,因此三个布告板类型都持有 weatherData
的引用以及需要关心的目标状态。
详细代码见:observer_mode_sample/sample2
observer
class observer {
public:
virtual void update() = 0;
};
current_condition_display_board
class currentConditionDisplayBoard: public observer {
private:
double temperature;
double humidity;
weatherData &wd;
public:
currentConditionDisplayBoard(weatherData &wd): wd(wd) { /* Initialize other member variables to 0 */ }
void update() {
double temperature = wd.getTemperature();
double humidity = wd.getHumidity();
if (util::equal(temperature, this->temperature) && util::equal(humidity, this->humidity)) {
return ;
}
this->temperature = temperature;
this->humidity = humidity;
printf("=========== Current Condition Display Board ============\n");
// output udpate data
}
};
statistics_display_board
class statisticsDisplayBoard: public observer {
private:
double minTemperature;
double maxTemperature;
weatherData &wd;
public:
statisticsDisplayBoard(weatherData &wd): wd(wd) {
minTemperature = std::numeric_limits<double>::infinity();
maxTemperature = -minTemperature;
}
void update() {
double temperature = wd.getTemperature();
if (temperature >= minTemperature && temperature <= maxTemperature) {
return ;
}
maxTemperature = std::max(maxTemperature, temperature);
minTemperature = std::min(minTemperature, temperature);
printf("=============== Statistics Display Board ===============\n");
// output update data
}
};
aqi_display_board
class aqiDisplayBoard: public observer {
private:
double aqi;
weatherData &wd;
public:
aqiDisplayBoard(weatherData &wd): wd(wd) { /* Initialize other member variables to 0 */ }
void update() {
double aqi = wd.getAQI();
if (util::equal(aqi, this->aqi)) {
return ;
}
this->aqi = aqi;
printf("================== AQI Display Board ===================\n");
// output update data
}
};
subject
class subject {
private:
std::vector<observer*> os;
public:
void attach(observer *o) {
os.push_back(o);
}
void detach(observer *o) {
os.erase(std::remove(os.begin(), os.end(), o), os.end());
}
void notify() {
for (auto o: os) {
o->update();
}
}
};
weather_data
class weatherData: public subject {
private:
int updateTimes;
double temperature;
double humidity;
double aqi;
public:
weatherData() { /* Initialize member variables to 0 */ }
double getTemperature() { return temperature; }
double getHumidity() { return humidity; }
double getAQI() { return aqi; }
void mesurementChange(double *temperature, double *humidity, double *aqi) {
if (temperature != NULL) {
this->temperature = *temperature;
}
if (humidity != NULL) {
this->humidity = *humidity;
}
if (aqi != NULL) {
this->aqi = *aqi;
}
++updateTimes;
printf("Update case %d:\n", updateTimes);
notify();
}
};
main
int main() {
double temperature;
double humidity;
double aqi;
weatherData wd;
currentConditionDisplayBoard ccBoard(wd);
statisticsDisplayBoard sBoard(wd);
aqiDisplayBoard aqiBoard(wd);
wd.attach(&ccBoard);
wd.attach(&sBoard);
wd.attach(&aqiBoard);
// ccBoard 与 sBoard 都会更新,aqiBoard 不会更新
temperature = 25;
humidity = 0.9;
wd.mesurementChange(&temperature, &humidity, NULL);
temperature = 26;
wd.mesurementChange(&temperature, NULL, NULL);
// ccBoard 会更新,sBoard 和 aqiBoard 都不会更新
temperature = 25.5;
wd.mesurementChange(&temperature, NULL, NULL);
// ccBoard 和 sBoard 都不会更新,aqiBoard 会更新
aqi = 50;
wd.mesurementChange(NULL, NULL, &aqi);
// aqiBoard 取消订阅,关于 aqi 的更新将不会触发 aqiBoard 的刷新显示
wd.detach(&aqiBoard);
aqi = 300;
wd.mesurementChange(NULL, NULL, &aqi);
return 0;
}
运行结果:
Update case 1:
=========== Current Condition Display Board ============
temperature: 25.0 humidity: 0.9
=============== Statistics Display Board ===============
max temperature: 25.0 min temperature: 25.0
Update case 2:
=========== Current Condition Display Board ============
temperature: 26.0 humidity: 0.9
=============== Statistics Display Board ===============
max temperature: 26.0 min temperature: 25.0
Update case 3:
=========== Current Condition Display Board ============
temperature: 25.5 humidity: 0.9
Update case 4:
================== AQI Display Board ===================
aqi: 50.0 level: 1
Update case 5:
从代码中可以看出,后续若有新布告板需要监听 weatherData
中数据的变更,只要新增一个布告板类,并继承 observer
抽象类即可,即使原布告板需要关注新的变更字段,也无需修改 weatherData
类中的任何一行代码,只需要修改对应布告板中的代码。达到了面向扩展开放,面向修改封闭的目的。
weatherData
类只依赖 observer
抽象类,而不依赖观察者类的具体实现,观察者与目标对象其中一方的改变并不会影响另一方,实现了松耦合。
从 main
函数中可以看到,由于 subject
提供了 attach
和 detach
方法,这使得我们可以在运行时动态地添加、删除观察者,不因将观察逻辑写死在代码中而限制这种绑定关系。
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
相关的字段,而统计布告板由于始终需要记录历史最高 / 最低温度,因此仍然不能取消 maxTemperature
与 minTemperature
成员变量。
详细代码见:observer_mode_sample/sample3
subject
typedef int aspect;
class subject {
private:
class observerWithAspect {
private:
observer *o;
std::unordered_set<aspect> as;
public:
observerWithAspect(observer *o, const std::initializer_list<aspect> &al): o(o) {
for (auto a: al) {
this->as.insert(a);
}
}
friend class subject;
};
std::vector<observerWithAspect*> owas;
static const int npos = -1;
public:
~subject() {
for (auto owa: owas) {
delete owa;
}
}
void attach(observer *o, std::initializer_list<aspect> al) {
observerWithAspect *owa = new observerWithAspect(o, al);
owas.push_back(owa);
}
void detach(observer *o, std::initializer_list<aspect> al) {
int pos = indexOfObserver(o);
if (pos == npos) {
return ;
}
for (auto a: al) {
owas[pos]->as.erase(a);
}
if (owas[pos]->as.empty()) {
delete owas[pos];
owas.erase(owas.begin() + pos);
}
}
void notify(const std::vector<aspect> &as) {
for (auto owa: owas) {
for (auto a: as) {
if (owa->as.find(a) != owa->as.end()) {
owa->o->update();
break;
}
}
}
}
};
weather_data
class weatherData: public subject {
private:
int updateTimes;
double temperature;
double humidity;
double aqi;
public:
static const aspect aspectTemperature;
static const aspect aspectHumidity;
static const aspect aspectAQI;
weatherData() { /* Initialize member variables to 0 */ }
void mesurementChange(double *temperature, double *humidity, double *aqi) {
std::vector<aspect> as;
if (temperature != NULL) {
this->temperature = *temperature;
as.push_back(this->aspectTemperature);
}
if (humidity != NULL) {
this->humidity = *humidity;
as.push_back(aspectHumidity);
}
if (aqi != NULL) {
this->aqi = *aqi;
as.push_back(aspectAQI);
}
++updateTimes;
printf("Update case %d:\n", updateTimes);
notify(as);
}
};
const aspect weatherData::aspectTemperature = 0;
const aspect weatherData::aspectHumidity = 1;
const aspect weatherData::aspectAQI= 2;
main
int main() {
double temperature;
double humidity;
double aqi;
weatherData wd;
currentConditionDisplayBoard ccBoard(wd);
statisticsDisplayBoard sBoard(wd);
aqiDisplayBoard aqiBoard(wd);
wd.attach(&ccBoard, {wd.aspectTemperature, wd.aspectHumidity});
wd.attach(&sBoard, {wd.aspectTemperature});
wd.attach(&aqiBoard, {wd.aspectAQI});
// bug fix:现在可以触发所有更新了
temperature = 0;
humidity = 0;
aqi = 0;
wd.mesurementChange(&temperature, &humidity, &aqi);
// ccdBoard 和 sdBoard 都会更新,aqiBoard 不会更新
temperature = 25;
humidity = 0.9;
wd.mesurementChange(&temperature, &humidity, NULL);
temperature = 26;
wd.mesurementChange(&temperature, NULL, NULL);
// ccdBoard 会更新,sdBoard 和 aqiBoard 都不会更新
temperature = 25.5;
wd.mesurementChange(&temperature, NULL, NULL);
// ccdBoard 和 sdBoard 都不会更新,aqiBoard 会更新
aqi = 50;
wd.mesurementChange(NULL, NULL, &aqi);
// aqiBoard 取消订阅,关于 aqi 的更新将不会触发 aqiBoard 的刷新显示
wd.detach(&aqiBoard, {wd.aspectAQI});
aqi = 300;
wd.mesurementChange(NULL, NULL, &aqi);
return 0;
}
observer
及三个布告板的实现尽管有些许变动,但读者可以通过上面的类图了解其实现,完整代码请移步:observer_mode_sample/sample3;initializer_list
是 c++11
提供的新特性,在这里用于实现可变个数参数的传参,在调用时可以传入任意数量的 aspect
,实际上可以用 vector
代替;subject
将 observerWithAspect
作为其私有内部类,原因是 observerWithAspect
类只用于拼装 observer
与 aspect
,不应该被除 subject
以外的其他类型所调用。每当 weatherData
类监测到数据变更时,将全量的变更消息通过 notify
中的 void*
参数推送出去,由布告板通过接收到的消息来判断自己所关心的变化是否发生了,如果发生,则刷新布告板的内容。
在 c++
中,void*
可以与任意类型的指针互相转换,这样就可以将变更信息通过一个结构体指针传出,当然 void*
转换前后必须是相同的结构体指针类型,否则程序可能出现严重错误。也可以将 void*
改为 char
数组,使用序列化、反序列化的方式来进行变更信息的转化,所有接收通知的布告板都需要遵守 weatherData
定下的变更通知信息的转化规则,才能收到正确的信息。
注意到这里已经不存在布告板到 weatherData
实例的引用,weatherData
类也不再对外提供 getState
方法,因为在布告板收到通知时,已经知道了所有的变更信息,不需要再通过 weatherData
的引用来获取它的最新状态了。
详细代码见:observer_mode_sample/sample4
observer
class observer {
public:
virtual void update(const void* changeInfo) = 0;
};
aqi_display_board
class aqiDisplayBoard: public observer {
private:
double aqi;
bool getInfo(const void *changeInfo) {
const weatherData::changeInfo *ci = reinterpret_cast<const weatherData::changeInfo*>(changeInfo);
if (ci->aqi == NULL) {
return true;
}
aqi = *(ci->aqi);
return false;
}
public:
aqiDisplayBoard() { /* Initialize member variables to 0 */ }
void update(const void *changeInfo) {
bool skip = getInfo(changeInfo);
if (skip) {
return ;
}
printf("================== AQI Display Board ===================\n");
printf("\taqi: %.1f\t\tlevel: %d\n\n", aqi, getLevel());
}
};
subject
class subject {
private:
std::vector<observer*> os;
public:
void notify(const void *changeInfo) {
for (auto o: os) {
o->update(changeInfo);
}
}
};
weather_data
class weatherData: public subject {
private:
int updateTimes;
double temperature;
double humidity;
double aqi;
public:
struct changeInfo {
double *temperature;
double *humidity;
double *aqi;
changeInfo(double *temperature, double *humidity, double *aqi):
temperature(temperature), humidity(humidity), aqi(aqi) {}
};
weatherData() { /* Initialize member variables to 0 */ }
void mesurementChange(double *temperature, double *humidity, double *aqi) {
changeInfo *ci = new changeInfo(temperature, humidity, aqi);
if (temperature != NULL) {
this->temperature = *temperature;
}
if (humidity != NULL) {
this->humidity = *humidity;
}
if (aqi != NULL) {
this->aqi = *aqi;
}
++updateTimes;
printf("Update case %d:\n", updateTimes);
notify(ci);
}
};
subject
的 attach
与 detach
方法的实现与应用于案例中的实现完全相同,故没有展示,完整代码请移步:observer_mode_sample/sample4;changeInfo
使用 const void*
类型,目的是防止在多个 update
调用过程中,changeInfo
中的值被修改,造成严重影响;changeInfo
只储存关于 weatherData
的变更信息,且应该允许外部访问到 changeInfo
中的数据成员,因此将 changeInfo
作为 weatherData
的 public
内部结构体。subject
的 getState
方法拉取详细的信息。这种在发生变更时,只将最小通知发送给观察者,其他详细信息等待观察者询问的模型称为拉模型,如果观察者较多,则拉模型将导致多次观察者的询问行为,可能使程序效率低下;changeInfo
推送出去。这种在每次发生变更时,将变更信息推送给观察者,被观察者不需要关心其他观察者是否需要这些信息的模型称为推模型,推模型可能导致观察者对象变得难以复用。不论是推模型还是拉模型,都要注意的是:不要设计特定于某个观察者的更新协议,因为我们无法预料后续可能会出现的其他观察者。
在每次目标对象的状态发生改变时,就自己在内部调用 notify
方法通知被观察者
优点:目标的调用者无需在改变目标状态后,调用 notify
进行通知(不会忘记)
缺点:多次对同一个目标的更新可能发送多条更新信息,降低效率
将 notify
方法的调用交给目标的调用者,由调用者来确定在“适当的时机”进行调用
notify
,避免不必要的通知notify
的触发容易被调用者忘记可能存在一个观察者观察多个被观察者的情况,例如一个表格依赖多个数据源,或者是游戏中的成就系统,甚至可能存在多对多的情况(例如用户使用界面按钮点击之间的互斥关系,点击了某几个按钮,则与其互斥状态的按钮需要 disable)。当观察者和被观察者之间的依赖关系变得更复杂的时候,需要引入一个 ChangeManager 管理这种依赖关系,目前有两种 ChangeManager:
dagChangeManager 是 Mediator(中介者)模式的一个实例,通常情况下只需要一个 ChangeManager 且全局可见,此时也可以使用 Singleton(单例)模式。
如果被观察者允许某些观察者更新自身的状态,由于每个观察者都并不知道其他观察者的存在,因此它对改变被观察者所产生的影响与代价一无所知。如果依赖关系的定义或维护不当,可能造成错误的更新,而这种错误通常很难被捕捉到。
在发送通知之前需要确保被观察者的状态自身是一致的,因为在发出通知之后,观察者需要查询目标的状态,若此时自身状态不一致,则容易出现逻辑错误,下面这段代码就非常容易在无意中犯下这样的错误:
class baseSubject {
protected:
int value;
public:
void notify() {
printf("notify now!\nvalue: %d\n", value);
}
void operation(int newValue) {
value = newValue;
notify();
}
};
class subject: public baseSubject {
public:
void operation(int newValue) {
baseSubject::operation(newValue);
// 通知已发送完毕
value += newValue;
}
};
/**
* 预期执行结果:
* notify now!
* value: 4
* 实际执行结果:
* notify now!
* value: 2
*/
int main() {
subject s;
s.operation(2);
return 0;
}
使用模板方法模式可以保证 notify
函数在所有状态确定之后才被调用:
class baseSubject {
protected:
int value;
public:
void notify() {
printf("notify now!\nvalue: %d\n", value);
}
virtual void doOperation(int newValue) {
value = newValue;
}
void operation(int newValue) {
doOperation(newValue);
notify();
}
};
class subject: public baseSubject {
public:
void doOperation(int newValue) {
baseSubject::doOperation(newValue);
value += newValue;
}
};
int main() {
subject s;
s.operation(2);
return 0;
}
观察者模式中的通知是同步进行的,只有所有观察者的操作都执行完毕之后,被观察者才会继续后面的操作,如果观察者进行的操作是比较耗时的,此时目标对象就会被观察者的流程阻塞,如果这种阻塞不是必须的,可以将观察者的操作推到另一个线程或工作队列中去。
在观察者模式中要十分小心其中混合的线程和锁,如果观察者试图获取目标的锁,这时就会产生死锁。在多线程引擎中,最好采取事件队列来做异步通信。
在 sample2 与 sample3 中,所有的观察者实例都持有一个 weatherData
的引用,而 sample4 中没有,sample2 与 3 中的 weatherData
作用是实现“拉模型”时需要通过该引用获取到目标状态,实际上可以将该引用放在 notify
中:
class subject {
public:
void notify(const weatherData &wd) {
for (auto o: os) {
o->update(wd);
}
}
};
观察者的 update
接口获取到该引用,就可以拿到目标状态。
实际上观察者保留指向被观察者的引用还有另一个更重要的作用:当观察者销毁时,它可以通过引用找到被观察者,取消自身在观察者的注册,如果被观察者的生命周期比观察者的周期更长,这是十分有必要的。
notify
时就会访问到非法地址;举个栗子,当玩家进入一个打怪副本时,会 new
出一个玩家的血条展示在界面的左上角,每当玩家受到攻击时,血条作为观察者就会收到一次攻击事件,并做出“扣血”的响应。当玩家退出副本后,副本中的血条要么被注销,要么仍然被目标对象(被观察者)引用,当玩家进入一个新副本时,每当他受到一次攻击,就会向上一个副本的血条发送一个攻击事件,而此时血条做出的任何更新响应都是无意义的。若观察者不是血条,而是受到攻击时播放声音的控件,此时玩家在受到攻击时就会听到双重攻击效果的声音,这种会产生副作用的通知,对玩家而言就是一个 bug。
同样,当被观察者的生命周期结束时,也应该向观察者发送通知,重置观察者到自身的引用,这可以通过向观察者发送一个“死亡”事件达到目的。
Observable
实现为一个具体的类,提供了setChanged
、notifyObservers
与 notifyObservers
方法,同时支持推拉模式,在调用 notify
方法之前需要先 setChanged
表示状态已改变,否则 notify
将不会通知观察者,在通知结束后,将会调用 clearChanged
将 changed
标记清除Listener
可以让 Swing
组件响应不同类型的事件(鼠标点击、键盘按键等事件)Signal
与 Slot
实现了组件之间的通信,每当一个 Signal
被发射,与其连接的 Slot
立即被执行,信号槽机制是 Qt 的核心机制,它是 Qt 自行定义的一种通信机制bilibili:23 个设计模式
git clone https://github.com/Dmaxiya/observer_mode_sample
temperature
或 humidity
值为 0,那么 ccBoard
与 sBoard
将不会刷新 ↩