一、背景
责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为型模式。
观察者模式(Observer Pattern),则是当一个对象被修改时,则会自动通知依赖它的对象。观察者模式属于行为型模式。
为什么把这两种模式放一起?是因为这两者在C语言里的实现比较接近,责任链是在接收到请求时,分别给接收对象进行处理,当处理的请求不是自身接收对象的请求时(或者是自身的责任范围内的请求,具体视责任链的定义来决定),则传递给下一个接收对象。所以对于责任链来说,只有在职责范围内的请求才会执行。而观察者模式,则是不管职责范围,全体都会执行。所以观察者模式可以看作是责任链的一种特殊情况(即所有条件均满足的情况)。
二、名词释义
既然提到链,那这里肯定会涉及链表,所以这里其实是两部分的内容,一个是责任的划分,一个是链表的表示。而对于观察者,则是少了一个责任划分的条件而已。
三、C语言例子
责任链
比如现在要做一个手机转账的功能,需求是当转账金额不大于100时,直接转,不需要任何提示;当转账金额大于100时,提示确认信息;当转账金额大于1000时,需要密码确认;当转账金额大于10000时,需要手机短信验证码确认。比如当转账金额为20000时,需要提示信息,并且密码确认,并且还需要手机短信验证。
我们先用常规思路来解答一下。
/* 无动作 */
extern void DoNothing(void);
/* 提示 */
extern void Tips(void);
/* 密码确认 */
extern void PswConfig(void);
/* 验证码确认 */
extern void CodeConfig(void);
void TransferFunc(uint32_t value)
{
if (value >= 10000)
{
CodeConfig();
}
else if (value >= 1000)
{
PswConfig();
}
else if (value >= 100)
{
Tips();
}
else
{
DoNothing();
}
}
上面这种实现其实没什么问题,但如果后面要加个操作,比如金额大于10w时,需要本人视频核对。那这个时候就需要去改动TransferFunc这个函数主体内容,一不小心可能还会把原本那些金额的处理给变更成其他操作。为了让执行逻辑与变更对象解耦开,这里使用责任链的方式。
/* 定义责任链的结构 */
struct tagTransfer
{
uint32_t Value;
void (*Func)(void);
struct tagTransfer *Next;
};
/* 无动作 */
extern void DoNothing(void);
/* 提示 */
extern void Tips(void);
/* 密码确认 */
extern void PswConfig(void);
/* 验证码确认 */
extern void CodeConfig(void);
/* 定义链头 */
struct tagTransfer *Head = NULL;
void TransferAddList(uint32_t value, void (*func)(void))
{
struct tagTransfer *last = Head;
struct tagTransfer *next = malloc(sizeof(struct tagTransfer));
if (NULL != next)
{
next->value = value;
next->func = func;
/* 按责任等级排个序 */
if (NULL == last)
{
last = next;
}
else
{
for (;NULL != last->next; last = last->next)
{
/* 按从大到小排序 */
if (value > last->next->value)
{
next->next = last->next;
last->next = next;
return ;
}
}
last->next = next;
}
}
}
/* 责任链执行判断 */
void TransferFunc(uint32_t value)
{
struct tagTransfer *last = Head;
for (;NULL != last; last = last->next)
{
if (value > last->value)
{
last->func();
}
else
{
break;
}
}
}
但对于嵌入式端,没必要使用动态链接的形式增减责任链内容,这里有另一种更适用于嵌入式的静态链接,没错,就是我们的老朋友表驱动。下面用表驱动的例程来展示下。
/* 定义责任链的结构 */
struct tagTransfer
{
uint32_t Value;
void (*Func)(void);
};
/* 无动作 */
extern void DoNothing(void);
/* 提示 */
extern void Tips(void);
/* 密码确认 */
extern void PswConfig(void);
/* 验证码确认 */
extern void CodeConfig(void);
/* 做好责任划分的静态定义 */
static const struct tagTransfer FuncTable[] =
{
{0, DoNothing},
{100, Tips},
{1000, PswConfig},
{10000, CodeConfig},
};
/* 责任链执行判断 */
void TransferFunc(uint32_t value)
{
for (uint8_t i = 0; i < sizeof(FuncTable) / sizeof(FuncTable[0]); i++)
{
if (value >= FuncTable[i].Value)
{
FuncTable[i].Func();
}
}
}
观察者
对于观察者模式,可以使用同样的结构,只是使用的场景不大一样,一般需要订阅、通知的场景,就像公众号,只要订阅了公众号,当公众号有新的推文时,会直接通知到所有订阅这个公众号的人。当前技术中,有使用这种方式的,比较典型的,就是MQTT协议。
这里我们也来做个小例子,比如现在有个触摸屏,当点击屏幕的按键时,需要显示按键被按下的状态,另外还需要响一下蜂鸣器。那我们先用常规思路处理一下。
/* 按键状态 */
enum emKeyState
{
KEY_STA_bounce = 0, /* 按键弹起 */
KEY_STA_press = 1, /* 按键按下 */
};
extern uint8_t KeySta = KEY_STA_bounce;
/* 蜂鸣器动作 */
extern void BuzzerFunc(void);
/* 显示屏刷新 */
extern void DisplayFunc(void);
void main(void)
{
while(1)
{
/* 按键按下时动作 */
if (KEY_STA_press == KeySta)
{
/* 蜂鸣器响 */
BuzzerFunc();
/* 界面刷新 */
DisplayFunc();
}
}
}
如果这时候需要再执行一个点亮LED的动作,那就需要找到按键按下这个判断条件,增加一个点亮LED的动作。
/* 按键状态 */
enum emKeyState
{
KEY_STA_bounce = 0, /* 按键弹起 */
KEY_STA_press = 1, /* 按键按下 */
};
extern uint8_t KeySta = KEY_STA_bounce;
/* 蜂鸣器动作 */
extern void BuzzerFunc(void);
/* 显示屏刷新 */
extern void DisplayFunc(void);
/* 点亮LED灯 */
extern void LEDFunc(void);
void main(void)
{
while(1)
{
/* 按键按下时动作 */
if (KEY_STA_press == KeySta)
{
/* 蜂鸣器响 */
BuzzerFunc();
/* 界面刷新 */
DisplayFunc();
/* 点亮LED灯 */
LEDFunc();
}
}
}
上面这种写法有问题么?实现起来没问题,也添加功能也很方便,但是有个问题,就是如果现在想把按键的功能封装起来放一个模块里,这时候按键触发的执行动作应该怎么处理?总不能把功能也封装进去,那以后每加一个功能就得改一次模块。所以这里就要用到观察者模式了。
这里的按键按下,可以看作是一个事件,当触发这个事件时,需要执行蜂鸣器响、界面刷新、点亮LED灯等操作。其实就是当发生了按键按下的事件时,需要同步通知蜂鸣器、界面和LED同步动作。这个模式在嵌入式里其实就是一个回调函数的应用。下面我们来看下怎么实现。
/*************************按键模块的实现.c****************************/
/* 按键状态 */
enum emKeyState
{
KEY_STA_bounce = 0, /* 按键弹起 */
KEY_STA_press = 1, /* 按键按下 */
};
/* 观察者链表结构 */
struct tagKeyFunc
{
void (*Func)(void);
struct tagKeyFunc *Next;
};
struct tagKeyFunc *Last = NULL;
uint8_t KeySta = KEY_STA_bounce;
/* 回调函数注册 */
void KeyPress_AddFunc(void(*func)(void))
{
struct tagKeyFunc *next = malloc(sizeof(struct tagKeyFunc));
next->Func = func;
next->Next = Last;
Last = next;
}
/* 事件触发时执行注册的回调函数 */
void KeyEventFunc(void)
{
struct tagKeyFunc *cur = Last;
/* 按键按下时,执行所有注册的功能 */
if (KEY_STA_press == KeySta)
{
for (; NULL != cur; cur = cur->Next)
{
cur->Func();
}
}
}
/********************************************************************/
/***************************应用.c*********************************/
extern void KeyPress_AddFunc(void(*func)(void));
extern void KeyEventFunc(void);
/* 蜂鸣器动作 */
extern void BuzzerFunc(void);
/* 显示屏刷新 */
extern void DisplayFunc(void);
/* 点亮LED灯 */
extern void LEDFunc(void);
void main(void)
{
/* 按键触发功能注册 */
KeyPress_AddFunc(BuzzerFunc);
KeyPress_AddFunc(DisplayFunc);
KeyPress_AddFunc(LEDFunc);
while(1)
{
KeyEventFunc();
}
}
/********************************************************************/
四、适用范围
责任链,适用于不同操作对象各自有明确的职责划分。
观察者,适用于需要遍历通知的场景。
五、优劣势
- 优势
- 两者都有一个比较明显的优势,就是把触发事件和执行功能两者解耦开。
- 如果使用链式结构,可以很方便地进行动态添加和删除。
- 劣势
- 功能执行的位置比较有局限性,不灵活,比如上面例子,所有功能只能在按键按下时才能执行,如果需要在弹起时执行则不能实现,需要另外增加一个弹起执行的回调,也就是功能执行的位置完全取决于事件开放的位置。
- 使用回调时,不容易发现无限递归的情况,即使用时有A回调B,B回调A这种无限调用的风险。