文章首发于知乎专栏:
https://zhuanlan.zhihu.com/c_1136684995157577728
个人公众号TarysThink
观察者(Observer)模式,观察者是什么人,是一个冷眼观瞧的人,这个人距离事件的中心很远,也就是说观察者与信息发布者之间能够解耦,这应该是观察者模式的核心。
有这么个场景,A是一个模块,A模块内有一个数据a,这个数据a是需要模块B获取的信息。我们把A叫做信息的生产者或者拥有者,模块B叫消费者。模块A和模块B的数据结构定义如下:
typedef struct A
{
a;
}A;
typedef struct B
{
aFromA;
}B;
a的内容发生改变后,A模块需要把a的值尽快通知给B模块。程序员李逵的实现方式如下:
//模块A
ChangeA();//模块A的字段a发生变化
PutToFIFO(A.a);
//模块B
RcvFIFOFromA(msg);
即模块A将值a写入消息队列,模块B运行时去对应位置取内容解析。
此时需求发生了变化。需求要求一旦模块A更新了a,必须马上通知模块B更新字段aFromA。我们再看一下李逵同学的第一版实现,模块A运行的时候向队列里写入值,直到模块B运行的时候aFromA才会得到更新,是不满足“马上通知”要求的。李逵想了想,改了第二版:
#include "B_interface.h"
//模块A
ChangeA();//模块A的字段a发生变化
UpdateValueAFromA();//调用模块B提供的函数UpdateValueAFromA
大多数这种场景的代码应该都是这么写的。如果B_interface.h中仅提供单一、纯粹的函数调用接口,我认为也是可以接受的。设计原则里说,模块间应该依赖于接口,李逵同学这么写当然也是依赖于接口了。
这里有一个这个接口函数声明究竟应该放在A_interface.h里,还是放在B_Interface.h里的问题。接口放在不同的位置决定了依赖的方向。我是这么考虑的,这取决于A模块更稳定还是B模块更稳定,我们应该向稳定方向依赖,例如A模块改动频率小、模块稳定、模块抽象度高,那么就应该把接口访进A模块,让B模块去包含A模块提供的接口。
关于这种场景,按照上面这种写法已经足够了。模块A还可以提供信息给模块C和D,则UpdateValueAFromA的函数实现如下:
void UpdateValueAFromA()
{
UpdateValueAFromAToB();
UpdateValueAFromAToC();
UpdateValueAFromAToD();
}
李逵同学这么写的话,模块A还是依赖了模块B,服务提供方依赖信息消费方可不是一个好事情。有人会说,UpdateValueAFromA的声明写在模块A里,定义写在模块B里,由模块B包含模块A的头文件,就实现了A不依赖B,B依赖A了呀。实际上,这么写的话,确实能够让A依赖少一些,A更稳定些,但B还是得了解很多A相关的东西。所以这个场景能不能更抽象些,让A和B共同依赖这个抽象。
代码看来看去只有一个UpdateValueAFromA函数需要抽象。我们把这个函数抽象成另一个函数后,让A生产者去调用这个抽象函数,让B消费者去实现这个抽象函数。说的还不够清晰,我们来看代码:
//模块A初始化阶段
typedef void (*UpdateValueAFromA)();
UpdateValueAFromA update[3] = {UpdateValueAFromAToB, UpdateValueAFromAToC, UpdateValueAFromAToD};
//模块A运行阶段
ChangeA();//模块A的字段a发生变化
for(i = 0; i < 3; i++)
update[i]();//调用模块B、C、D提供的函数UpdateValueAFromA
从模块间依赖的角度,这段代码把模块A最复杂的运行阶段代码剥离了对其他模块的依赖,而把这部分放到了初始化阶段。它没有解决依赖,只是转移了依赖,虽然如此,但这也是个不错的办法。
依赖的问题暂时讨论到这里。这面这段代码利用了设计模式中的观察者模式,完整版的观察者模式还多了一些尾巴,这些尾巴有什么用呢?下面我们接着说。
此时来了新的需求,模块A还有一个字段b想提供给模块E、F,这代码交给李逵写,肯定又造一份一模一样结构的代码。那么这个代码结构可以复用吗?我们来看下面的结构定义:
//脱离于模块A和B,代码中某个infra中有如下定义
typedef struct Consumer
{
void (*update)(struct Consumer* consumer);
}Consumer;
typedef struct Producer
{
Consumer* consumerList[3];
}Producer;
那么,初始化和运行态代码如下:
//模块A初始化阶段
Producer aproducer;
aproducer.consumerList[0]->update = UpdateValueAFromAToB;
aproducer.consumerList[1]->update = UpdateValueAFromAToC;
aproducer.consumerList[2]->update = UpdateValueAFromAToD;
Producer bproducer;
bproducer.consumerList[0]->update = UpdateValueBFromAToE;
bproducer.consumerList[1]->update = UpdateValueBFromAToF;
//模块A运行阶段
ChangeA();//模块A的字段a发生变化
for(i = 0; i < 3; i++)
aproducer.consumerList[i]->update();//调用模块B、C、D提供的函数UpdateValueAFromA
ChangeB();
for(i = 0; i < 2; i++)
bproducer.consumerList[i]->update();
上述代码是观察者模式的一种最简单的场景。