C常用设计模式——观察者模式

文章首发于知乎专栏:

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();

上述代码是观察者模式的一种最简单的场景。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值