Observer模式的C实现

设计模式 (Design Pattern,以下简称DP)的定义有很多种。我个人的理解:DP是人们在软件开发过程中所总结出来的一些典型问题的经验解决方法模板。使用它们可以使我们的代码更易被复用,更易扩展,更好地适应变化以及更便于后期维护。

人们都说设计模式是独立于语言的,但这里的"语言"更多的是指面向对象语言,比如Java、C++、C#、Python和Ruby等。使用面向对象语言(OO)在实现设计模式时更为自然而然。GoF的经典书籍《Design Patterns》 的副标题就是"Elements of Reusable Object-Oriented Software",显然DP主要针对面向对象的软件开发,书中的内容也主要是用C++表述的。

相比于OO语言,关于C语言 等面向过程的语言与设计模式结合使用的资料和例子都甚少,它们就像是走在两条互相平行的马路上的路人,老死不相往来。难道我们真地找不到C与设计模式的交集吗,非也!设计模式强调高内聚,低耦合 ,一切面向接口!这种思想其实也有助于C程序员写出更加模块化、更加灵活以及更为优美的代码来。只是用C语言实现后所展现出来的形式与支持继承、多态的OO语言相比可能不是那么自然。

我相信在现实使用C语言的开发过程中,有些C程序员已经在不知不觉中使用了模式的思想,但DP对于多数C程序员还是略显生分的,虽然DP已经诞生10多年了,也许这与C语言诞生在设计模式之前不无关系^_^。

如何在日常开发中融入模式的思想呢?GoF在《Design Patterns》一书的第一章就告诉了我们,大致是一切从问题出发:弄清楚你遇到的问题,浏览模式,找到适合解决你的问题的模式。

言归正传。我们的系统常常有这样的业务情景:某种数据对象发生变化,与其相关的其他数据对象也需要一并做出改变。为了便于理解,这里举一个大家都比较易懂的例子:Tony是一个中国移动全球通(GoTone)用户,他订购了中国移动提供的139邮箱和手机报业务。中国移动有一个系统用于管理全球通用户信息、各种移动业务以及全球通用户的各种业务订购关系。Tony这个人记性不大好,每天丢三落四,同时也经常忘记及时缴纳手机话费,导致系统中Tony这个用户的状态经常在"正常"和"停机"间变来变去。为了防止Tony在停机的状态下依旧可以使用邮箱和手机报业务,移动公司提出了一个新需求:当用户处于"停机"状态时,用户应该无法使用139邮箱和手机报业务。如果你是负责开发这个需求的程序员,你如何在系统中满足这个需求呢?

将客户提出的需求翻译为程序员的行话就是当Tony的用户信息由"正常"变为"停机"时,系统需要同时修改139邮箱订购关系数据和手机报订购关系数据,将两类数据记录中Tony的订购关系由"开通"变为"暂停";当Tony的用户信息由"停机"变为"正常"时,系统则需要将两类数据记录中Tony的订购关系由"暂停"改为"开通"。总之,当全球通用户信息发生变化时,139邮箱订购关系与手机报订购关系数据就需要随着进行变更。

从例子中已有的描述可以看出,现有的系统内部至少有三套数据集以及相应的数据操作接口,分别是全球通用户数据、139邮箱订购关系数据和手机报订购关系数据,这也是最基本的数据封装与抽象。现在我们就在此基础上来满足新的需求。

很多人首先想到的是修改全球通用户数据操作接口,实现两类订购关系的随动变更。
void update_gotone_customer_state(const char *number, int state) {
    /* 根据number查找出对应的用户信息,并更新其state */
    … …

    /* 新增如下操作 */
    update_mailbox_order(number, state);
    update_newspaper_order(number, state);
}

我们可以看出这种方法是在全球通用户数据操作接口中直接调用两种订购关系的数据操作接口来修改数据状态,这种方法显然是耦合最高的方法,它在本无耦合的数据对象之间建立了耦合。update_mailbox_order和update_newspaper_order的变化将直接导致update_gotone_customer_state接口的变化。我们也无法独立地对update_gotone_customer_state接口进行单元测试了,除非对新增的两个依赖接口进行mock — 显然这种mock是被动的,也是不合理的。

为了去除这一方法引入的耦合,我们可以引入一个新函数来作为用户状态变更时的处理函数,如下:
void gotone_customer_state_switch(const char *number, int state) {
    update_gotone_customer_state(number, state);
    update_mailbox_order(number, state);
    update_newspaper_order(number, state);
}

这种方法依次调用三个数据集各自的操作接口更新用户状态。显然这种方法去除了数据集操作接口之间的耦合,但也存在着另外一个更严重的问题,那就是这种方法难以适应移动业务日新月异的变化。

假定此时用户Tony又订购了一个移动业务 -手机电视,那么如何让手机电视订购关系感知到用户状态的变化呢?你可能会这样来改。
void gotone_customer_state_switch(const char *number, int state) {
    update_gotone_customer_state(number, state);
    update_mailbox_order(number, state);
    update_newspaper_order(number, state);
    update_mobiletv_order(number, state);
}

你知道这样的修改意味着什么吗?意味着程序要重新编译,重新发布以及重新部署上线。一个用户新订购了一个业务就带来如此大的变动,这是不能忍受的,不是吗!

关于这类问题,DP给出了一个解决模式,即Observer模式,中文叫作观察者模式。观察者模式中有两个主要元素:主题(Subject)和观察者(Observer),主题的变更引发诸多相应观察者的随动更新。在这个例子中,全球通用户数据显然是一个Subject,而139邮箱、手机报以及手机电视等业务订购数据则是Observer的角色。

全球通用户数据是一个Subject,但是却是一个具体的Subject,如果让其与139邮箱等具体业务Observer直接关联,势必会像最初的解决方法那样在不同数据对象间引入耦合。DP始终在告诉我们要面向接口,要依赖抽象,所以我们需要建立抽象的Subject和Observer接口来。在C语言中没有interface关键字,也没有abstract class(抽象类),C语言只有struct和函数指针。将struct和函数指针结合,我们就有了C语言接口的概念:

/* isubject.h */
struct iobserver_t;

struct isubject_t {
    void (*attach)(struct isubject_t *subject, struct iobserver_t *observer);
    void (*detach)(struct isubject_t *subject, struct iobserver_t *observer);
    void (*notify)(struct isubject_t *subject, void *arg);
};

/* iobserver.h */
struct iobserver_t {
    void (*update)(void *arg);
};

isubject_t声明了三个函数指针字段,attach用于增加监视这个Subject的Observer的;detach用于卸载监视这个Subject的某个Obsever;而notify则是在Subject发生变化时用于通知各个Observer进行更新的。iobserver_t相对比较简单,只有一个update函数指针字段,该字段指向的函数在Subject发生变化时被调用,完成Observer自身的更新。

isubject_t只是一个抽象的接口,我们还需要isubject_t的一个具体实现,包括attach, detach以及notify等函数的具体实现算法。下面是isubject_t的一个具体实现isubject_imp_t的相关数据类型与操作接口:

/* isubject_imp.h */
struct isubject_t* isubject_imp_new();
void isubject_imp_free(struct isubject_t **subject);

/* isubject_imp.c */
typedef struct _iobserver_t _iobserver_t;

/* 这里用apache apr库中的apr_ring作为存储observers的数据结构 */
typedef APR_RING_HEAD(_iobserver_head_t, _iobserver_t) _iobserver_head_t;

struct isubject_imp_t {
    struct isubject_t subject; /* 这里务必将subject放在第一个字段的位置 */
    _iobserver_head_t observers;
};

struct isubject_t* isubject_imp_new() {
    struct isubject_imp_t *p = NULL;

    p = (struct isubject_imp_t*)malloc(sizeof(*p));
    if (!p) return NULL;

    memset(p, 0, sizeof(*p));
    APR_RING_INIT(&(p->observers), _iobserver_t, link);
    p->subject.attach = isubject_imp_attach;
    p->subject.detach = isubject_imp_detach;
    p->subject.notify = isubject_imp_notify;

    return (struct isubject_t*)p;
}

static void isubject_imp_attach(struct isubject_t *subject, struct iobserver_t *observer) {
    struct isubject_imp_t *p = (struct isubject_imp_t *)subject;

    //将observer插入ring,这里代码省略
}

static void isubject_imp_detach(struct isubject_t *subject, struct iobserver_t *observer) {
    struct isubject_imp_t *p = (struct isubject_imp_t *)subject;
   
    //将observer从ring中移出,这里代码省略
}
   
static void isubject_imp_notify(struct isubject_t *subject, void *arg) {
    struct isubject_imp_t *imp = (struct isubject_imp_t *)subject;

    //遍历ring,调用每个observer的update接口,并传入参数arg,这里代码省略
}

注意以上操作都是面向isubject_t这个接口类型的,而不是isubject_imp_t这个具体类型的,isubject_imp_t对于外部是不可见的。下面我们尝试用Observer模式解决一下上面例子中的问题。

首先建立tony这个用户的subject,并初始化其当前的业务Observer:
struct isubject_t *tony_subj = isubject_imp_new();
struct iobserver_t mailbox_observer = {
        .update = update_mailbox_order
};
struct iobserver_t newspaper_observer = {
        .update = update_newspaper_order
};

tony_subj->attach(tony_subj, &mailbox_observer);
tony_subj->attach(tony_subj, &newspaper_observer);

恰巧Tony上个月又忘记缴纳话费了,月初Tony的手机被停机了。中国移动的系统进行了Tony这个账户的状态变更操作:
tony_subj->notify(tony_subj, tony_account_info); /* tony_account_info表示Tony的基本账户信息 */

这行代码的执行结果是update_mailbox_order和upudate_newspaper_order被调用,139信箱以及手机报订购关系中有关Tony的订购信息状态变为"暂停"。也许上面的代码还没有让你看到Observer模式的好处,Ok,让我们进行一些变化!

移动为了推广3G业务,允许用户免费试用3个月的手机电视业务。有便宜不能不占,Tony遂订购了手机电视业务。前面的两种方法对于此种情况都无能为例,但采用了Observer模式之后,对于手机电视的订购,系统只需要在相应的处理函数中执行:
struct iobserver_t mobiletv_observer = {
        .update = update_mobiletv_order
};
tony_subj->attach(tony_subj, &mobiletv_observer);

这样一来我们无需修改Tony被停机时的处理代码,手机电视订购关系也可以感知到Tony的账户变更。

同样,Tony使用了移动的手机上网套餐,打开手机浏览器,天下信息便可一览无余。于是Tony决定退订有些鸡肋的手机报业务,对于这一退订事件,系统只需要在相应的处理函数中执行:
tony_subj->detach(tony_subj, &newspaper_observer);

可以看到Tony停机的处理无需因手机报退订而做出改变,同时Tony停机也不会导致手机报订购关系数据的变更,这正是我们期望的。

以上是Observer模式的一种C语言实现,同时也解决了我们遇到的实际问题。关于Observer模式的详细描述还是参见GoF的《Design Patterns》一书吧。用过程式语言实现DP的确不那么自然,但这就是C语言的方式。

秋游天华山

不知为何,一到秋天我就有了爬山的冲动。于是乎我和同事一行六人在一个秋高气爽的周末来到了位于丹东宽甸的天华山

辽宁境内的山我爬过的不多,之前只是去过千山和关门山。选择天华山也是再三考虑了同事们久疏于运动之后做出的决定,其实我个人更加向往征服另一座更为难爬的山峰 – 位于凤城的凤凰山,看来我的这个目标只能等来年再实现了^_^。

景区的自费倒站车将我们送到了此次登山的起点-通天峡广场。一上来我们就要征服据说长达300多米的通天峡。所谓通天峡其实就是一道山体的裂缝,景区在裂缝内搭建了人类可以爬行的云梯,这么长的"山体裂缝"我还是第一次遇见,爬起来很是刺激。

通天峡

天华山让我感觉最好的地方就是可以手脚并用,而不仅仅是一直攀登石头台阶,这才叫真正的爬山。由于双手的介入,爬山过程中腿部感觉并不是很累,这点与爬千山、关门山有很大不同。

这个季节天华山上已有些许枫叶变得红彤彤,一路之上我们也以发现红枫叶为最大的兴奋点,我们也的确收获了不少红叶美景。

山中红叶

忘记说了,爬天华山无论如何都不能缺少一种装备,那就是防滑手套。前面说过爬天华山最大的特点就是要"手脚并用",由于爬山过程较长,如果空手攀爬必然会对稚嫩的手带来一定的伤害,看看下面的照片你就能理解手套的功用了。

登山天梯

我一直走在最前面,一是为大家探探路,二是把握好爬山的节奏,激励大家爬山的斗志,最后就是在等待大家上来的那段时间独自远眺山景,充分体会大自然的壮丽。

山景远眺

天华山有多条游览路线,我们此行选择的目的地是天华山天台。天台应该不是整个景区的最高点,但却是名气最大的一处。天台,物如其名,就是山顶一处由山石构成的石台子,台子有一定倾角,但面积不小,可容纳几十号人。登上高台的瞬间感觉确实很棒,但在半山腰远观天台却更能凸显天台的无限魅力。

天华山天台

都说"上山容易,下山难",不过这次我们却没有这种感觉,下山的过程很顺利。呼吸着大山中纯净富氧的空气,我们一路说笑,每个人的脸上呈现出来的不是疲惫,而是无比地放松。

回到通天峡入口,离集合时间尚早,我们没有选择坐倒站车,而是继续步行下山。我们沿着从"爱之源"流下的山溪而下,一路欣赏着天华山美丽的水景。现在似乎是枯水期,想必在富水期,这里的景色会更美。

就这样,我完成了对天华山的征服^_^。

附具体行程(散客拼团):
- 早上4点多起床,5点集合。
- 5点30奔赴天华山
- 9点左右到达天华山景区
- 9点半上山
- 14:00下山
- 14:30返城
- 19:30回到温馨的家

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats