一、背景
責任鏈模式(Chain of )為請求創建了一個接收者對象的鏈。這種模式給予請求的類型,對請求的發送者和接收者進行解耦。這種類型的設計模式屬于行為型模式。
觀察者模式( ),則是當一個對象被修改時,則會自動通知依賴它的對象。觀察者模式屬于行為型模式。
為什么把這兩種模式放一起?是因為這兩者在C語言里的實現比較接近,責任鏈是在接收到請求時,分別給接收對象進行處理,當處理的請求不是自身接收對象的請求時(或者是自身的責任范圍內的請求,具體視責任鏈的定義來決定),則傳遞給下一個接收對象。所以對于責任鏈來說,只有在職責范圍內的請求才會執行。而觀察者模式,則是不管職責范圍,全體都會執行。所以觀察者模式可以看作是責任鏈的一種特殊情況(即所有條件均滿足的情況)。
二、名詞釋義
既然提到鏈,那這里肯定會涉及鏈表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時,需要本人視頻核對。那這個時候就需要去改動這個函數主體內容,一不小心可能還會把原本那些金額的處理給變更成其他操作。為了讓執行邏輯與變更對象解耦開,這里使用責任鏈的方式。
/* 定義責任鏈的結構 */
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;
}
}
}
但對于嵌入式端,沒必要使用動態鏈接的形式增減責任鏈內容c實現觀察者模式,這里有另一種更適用于嵌入式的靜態鏈接,沒錯,就是我們的老朋友表驅動。下面用表驅動的例程來展示下。
/* 定義責任鏈的結構 */
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 *Head;
uint8_t KeySta = KEY_STA_bounce;
/* 回調函數注冊 */
void KeyPress_AddFunc(void(*func)(void))
{
struct tagKeyFunc *last = Head;
/* 新注冊的功能插入鏈尾 */
for (; NULL != last; last = last->Next);

last->Func = func;
}
/* 事件觸發時執行注冊的回調函數 */
void KeyEventFunc(void)
{
struct tagKeyFunc *last = Head;
/* 按鍵按下時,執行所有注冊的功能 */
if (KEY_STA_press == KeySta)
{
for (; NULL != last; last = last->Next)
{
last->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這種無限調用的風險。