冒號(hào)課堂
第三課 常用范式(4)
3.4事件驅(qū)動(dòng)——有事我叫你,沒事別煩我
勞心者治人,勞力者治于人 ——《孟子·滕文公上》
關(guān)鍵詞:編程范式,事件驅(qū)動(dòng)式,回調(diào)函數(shù),framework,IoC,DIP,觀察者模式
摘要:事件驅(qū)動(dòng)式編程簡(jiǎn)談
?提問
- 什么是事件?有哪些不同類型的事件?
- 什么是回調(diào)函數(shù)?什么是異步同調(diào)?它們有什么用處?
- 控制反轉(zhuǎn)的目的是什么?它是如何實(shí)現(xiàn)的?在框架設(shè)計(jì)中起什么作用?
- 控制反轉(zhuǎn)、依賴反轉(zhuǎn)原則和依賴注射的共同點(diǎn)是什么?
- 事件驅(qū)動(dòng)式編程有哪些關(guān)鍵步驟?
- 異步過程特點(diǎn)和作用是什么?
- 事件驅(qū)動(dòng)式編程最重要的特征是什么?它們是如何實(shí)現(xiàn)的?
- 事件驅(qū)動(dòng)式與觀察者模式、MVC模型有何關(guān)系?
:講解
逗號(hào)漸覺睡蟲上腦,開始閉目點(diǎn)頭。正神游之際,忽覺腰間一陣酥麻。惺眼微睜,原是被引號(hào)的胳膊肘給捅的,頓時(shí)警醒。抬頭見講臺(tái)上的老冒正目光灼灼地盯著自己,不禁臉頰微燙,囁嚅道:“不好意思,昨晚睡得太晚了。”
冒號(hào)卻不以為意:“正愁找不到新話題呢,你倒啟發(fā)我了。話說課堂上睡覺大抵有三種方式——”
話音未落,有人已笑不自禁。
“第一種是警覺式:想睡可又擔(dān)心被老師發(fā)現(xiàn),不時(shí)睜眼查看周圍的變化。同時(shí)雙耳保持警戒,一有異動(dòng)立刻挺直身板。”冒號(hào)有板有眼地形容,“第二種是寬心式:俯桌酣睡,如處無人之境。境界至高者或可雷打不動(dòng),或可鼾聲如雷。”
“總之是很雷人。”嘆號(hào)的網(wǎng)絡(luò)新語再度引發(fā)笑聲。
冒號(hào)繼續(xù)分析:“第三種是托付式:請(qǐng)人放哨,非急勿擾。遂再無顧忌,大可封目垂耳,安心入眠。請(qǐng)問你們樂意采用哪種方式?”
“第一種方式睡不踏實(shí),不得已而為之。敢用第二種方式的人多半沒心沒肺,估計(jì)IT人都達(dá)不到那種境界。只要有同伴在身旁,我想大家都會(huì)選第三種方式的。”句號(hào)的回答獲得一致認(rèn)同。
冒號(hào)續(xù)問:“好,拋開第二種方式不談,為什么第三種要比第一種優(yōu)越呢?”
句號(hào)回答:“犯困者既要打盹又要警戒,必然苦不堪言。如果把警戒的任務(wù)委托同伴,兩人分工合作,自然愉快得多。”
冒號(hào)再問:“他們是如何合作的呢?”
“放哨者一旦發(fā)現(xiàn)有情況,立即通知犯困者采取行動(dòng)——睜眼坐直,作認(rèn)真聽講狀。”句號(hào)說得是繪聲繪色。
除了兩位當(dāng)事人略顯尷尬外,其他人均樂不可支。
眼見時(shí)機(jī)成熟,冒號(hào)不再兜圈:“采用警覺式者主動(dòng)去輪詢(polling),行為取決于自身的觀察判斷,是流程驅(qū)動(dòng)的,符合常規(guī)的流程驅(qū)動(dòng)式編程(Flow-Driven Programming)的模式。采用托付式者被動(dòng)等通知(notification),行為取決于外來的突發(fā)事件,是事件驅(qū)動(dòng)的,符合事件驅(qū)動(dòng)式編程(Event-Driven Programming,簡(jiǎn)稱EDP)的模式。下面我們就來說說這種編程范式。”
逗號(hào)甕聲甕氣道:“沒想到打瞌睡打出了個(gè)范式。”
冒號(hào)瞥了他一眼,繼續(xù)說下去:“為完成一樣事,既可以采用流程驅(qū)動(dòng)式,也可以采用事件驅(qū)動(dòng)式。這樣的例子在生活中可謂俯拾即是,剛才逗號(hào)同學(xué)為大家現(xiàn)場(chǎng)示范了一個(gè),誰還能舉出其他范例?”
嘆號(hào)搶先舉例:“與客戶打交道,推銷員主動(dòng)打電話或登門拜訪,他的工作是流程驅(qū)動(dòng)的;接線員坐等電話,他的工作是事件驅(qū)動(dòng)的。”
問號(hào)也說:“同樣是交通工具,公共汽車主要是流程驅(qū)動(dòng)的,它的路線已預(yù)先設(shè)定;出租車主要是事件驅(qū)動(dòng)的,它的路線基本上由隨機(jī)搭載的乘客所決定。”
引號(hào)以個(gè)人經(jīng)驗(yàn)作例:“購買喜愛的雜志可以選擇頻繁光顧報(bào)刊亭,也可以選擇一次性訂閱。瀏覽關(guān)注的新聞網(wǎng)站或博客,可以直接訪問站點(diǎn),也可以訂閱相應(yīng)的RSS。主動(dòng)檢查所關(guān)心的內(nèi)容是否更新是流程驅(qū)動(dòng)的,用訂閱的方式是事件驅(qū)動(dòng)的。”
句號(hào)回到本行:“Windows下的許多工作既可以在DOS下用批處理程序?qū)崿F(xiàn),也可以在圖形界面下完成。前者不需人工干預(yù),顯然是流程驅(qū)動(dòng)的;后者毫無疑問是事件驅(qū)動(dòng)的。”
“看來你們對(duì)這種范式很熟悉嘛。不過,它原理雖簡(jiǎn)單,威力卻無窮。看似一招,實(shí)則暗藏百式,甚可幻化千招。個(gè)中精妙之處,斷非一時(shí)可以盡述。”冒號(hào)不知不覺中又走進(jìn)了武俠的世界。
眾人聽了,暗疑老冒有些言過其實(shí)。
冒號(hào)正式入題:“首當(dāng)其沖的問題是:何謂事件?通俗地說,它是已經(jīng)發(fā)生的某種令人關(guān)注的事情。在軟件中,它一般表現(xiàn)為一個(gè)程序的某些信息狀態(tài)上的變化。基于事件驅(qū)動(dòng)的系統(tǒng)一般提供兩類的內(nèi)建事件(built-in event):一類是底層事件(low-level event)或稱原生事件(native event),在用戶圖形界面(GUI)系統(tǒng)中這類事件直接由鼠標(biāo)、鍵盤等硬件設(shè)備觸發(fā);一類是語義事件(semantic event),一般代表用戶的行為邏輯,是若干底層事件的組合。比如鼠標(biāo)拖放(drag-and-drop)多表示移動(dòng)被拖放的對(duì)象,由鼠標(biāo)按下、鼠標(biāo)移動(dòng)和鼠標(biāo)釋放三個(gè)底層事件組成。”
問號(hào)推想:“編程人員應(yīng)該還能創(chuàng)造新的事件類型吧?”
“那是當(dāng)然。”冒號(hào)點(diǎn)點(diǎn)頭,“還有一類用戶自定義事件(user-defined event)。它們可以是在原有的內(nèi)建事件的基礎(chǔ)上進(jìn)行的包裝,也可以是純粹的虛擬事件(virtual event)。除此之外,編程者不但能定義事件,還能產(chǎn)生事件。雖然大部分事件是由外界激發(fā)的自然事件(natural event),但有時(shí)程序員需要主動(dòng)激發(fā)一些事件,比如模擬用戶鼠標(biāo)點(diǎn)擊或鍵盤輸入等,這類事件被稱為合成事件(synthetic event)[1]。這些都進(jìn)一步豐富完善了事件體系和事件機(jī)制,使得事件驅(qū)動(dòng)式編程更具滲透性。”
嘆號(hào)嘟噥了一句:“看來這里邊還有點(diǎn)名堂。”
“名堂多著呢!”冒號(hào)回應(yīng),“事件固然是事件驅(qū)動(dòng)式編程的核心概念,但一個(gè)編程范式的獨(dú)特之處絕不僅僅是一些概念,更重要的是建立于這些概念之上的思維模式。為了了解這種范式與眾不同的特點(diǎn),我們先看看如何利用win32的API在windows下創(chuàng)建一個(gè)簡(jiǎn)單的窗口——”
/** 一個(gè)win32窗口程序 */
…WinMain(...) // windows應(yīng)用程序的主函數(shù)
{
// 第一步——注冊(cè)窗口類別
...;
windowClass.lpfnWndProc = WndProc; // 指定該類窗口的回調(diào)函數(shù)
windowClass.lpszClassName = windowClassName; // 指定該類窗口的名字
RegisterClassEx(&windowClass);
//第二步——?jiǎng)?chuàng)建一個(gè)上述類別的窗口
CreateWindowEx(…, windowClassName, ...);
…;
// 第三步——消息循環(huán)
while (GetMessage(&msg, NULL, 0, 0) > 0) // 獲取消息
{
TranslateMessage(&msg); // 翻譯鍵盤消息
DispatchMessage(&msg); // 分派消息
}
}
// 第四步——窗口過程(處理消息)
…WndProc(…, msg,...)
{
switch (msg)
{
case WM_SIZE: …; // 用戶改變窗口尺寸
case WM_MOVE: …; // 用戶移動(dòng)窗口
case WM_CLOSE: …; // 用戶關(guān)閉窗口
…;
}
}
“沒有選用Java、Visual C++、C#、VB或者Delphi來實(shí)現(xiàn)窗口,是因?yàn)樗鼈兏叨鹊姆庋b和強(qiáng)大的IDE掩蓋了部分事件機(jī)制。如果你們對(duì)win32 API不太熟悉,沒有關(guān)系。為了減少語言和API上的障礙,同時(shí)突出重點(diǎn),這里最大限度地省略了次要的過程和參數(shù)等,僅保留脈絡(luò)主干。”冒號(hào)解釋,“從中看出到,創(chuàng)建一個(gè)能響應(yīng)用戶操作的win32窗口共分四步:注冊(cè)窗口類別、創(chuàng)建窗口、消息循環(huán)和窗口過程。”
問號(hào)對(duì)概念很敏感:“消息與事件是一回事嗎?”
“嚴(yán)格說來它們不是一回事,但如果你不想深究,不加區(qū)分也無大礙。概略地說,消息是Windows內(nèi)部最基本的通訊方式,事件需要通過消息來傳遞,是消息的主要來源。每當(dāng)用戶觸發(fā)一個(gè)事件,如移動(dòng)鼠標(biāo)或敲擊鍵盤,系統(tǒng)都會(huì)將其轉(zhuǎn)化為消息并放入相應(yīng)程序的消息隊(duì)列(message queue)中[2]。”冒號(hào)解答著,“明白了這一點(diǎn),上面的代碼就不難理解了——在消息循環(huán)中,程序通過GetMessage不斷地從消息隊(duì)列中獲取消息,經(jīng)過TranslateMessage預(yù)處理后再通過DispatchMessage將消息送交窗口過程WndProc處理。”
逗號(hào)琢磨了一會(huì),不解地問:“窗口過程應(yīng)該是在分派消息時(shí)被調(diào)用的,但我怎么想不出DispatchMessage是如何聯(lián)系到WndProc的?”
冒號(hào)為其解惑:“DispatchMessage的消息參數(shù)含有事發(fā)窗口的句柄(handle),從而可以得到窗口過程WndProc[3]。至于窗口與窗口過程之間是如何建立聯(lián)系的,回看前面兩步就一目了然了:當(dāng)初在創(chuàng)建窗口時(shí)指明了窗口類別名windowClassName,而窗口類別windowClass又綁定了窗口過程。”
嘆號(hào)有點(diǎn)納悶:“干嘛要繞這么大的彎子,直接調(diào)用WndProc不就得了?”
“對(duì)于這個(gè)簡(jiǎn)單的程序來說,的確區(qū)別不大。但假如再增添其他菜單、按鈕、文本框之類的控件,每個(gè)控件都可綁定自己的窗口過程,那么到底該調(diào)用哪個(gè)才對(duì)呢?”冒號(hào)反問。
嘆號(hào)雖有所悟,但仍有心結(jié):“總覺得窗口過程的用法有些怪怪的。”
冒號(hào)一敲桌案:“沒錯(cuò)!怪就怪在編程者自己寫了一個(gè)應(yīng)用層的函數(shù),卻不直接調(diào)用它,而是通過庫函數(shù)間接調(diào)用。這類函數(shù)有個(gè)專用名稱:回調(diào)函數(shù)(callback)。”
引號(hào)忍不住插話:“回調(diào)函數(shù)我知道,在C和C++中就是函數(shù)指針嘛。”
“確切地說,函數(shù)指針是C和C++用來實(shí)現(xiàn)callback的一種方式。此外,C++中的functor、Java中的interface和C#中的delegate都可實(shí)現(xiàn)callback。我們先圖解一下回調(diào)機(jī)制。”冒號(hào)調(diào)出一張圖示——

“如果我們把系統(tǒng)劃分為兩層[4]:底層的函數(shù)庫和高層的應(yīng)用程序。同樣作為主函數(shù)的輔助函數(shù),左圖中的普通函數(shù)直接被主函數(shù)調(diào)用,然而右圖中的回調(diào)函數(shù)卻是通過庫函數(shù)間接被主函數(shù)調(diào)用的。”冒號(hào)的手影在幻燈下上下翻飛。
句號(hào)點(diǎn)出要害:“一般都是高層代碼調(diào)用低層代碼,callback反其道而行之,因此顯得與眾不同。”
“所言極是。一方面,在軟件模塊分層中,低層模塊為高層模塊提供服務(wù),但不能依賴高層模塊,以保證其可重用性和可擴(kuò)展性;另一方面,通常被調(diào)者(callee)為調(diào)用者(caller)提供服務(wù),調(diào)用者依賴被調(diào)者。兩相結(jié)合,決定了低層模塊多為被調(diào)者,高層模塊多為調(diào)用者。callback的出現(xiàn)改變了這種慣例,我們看一個(gè)簡(jiǎn)單的例子。”冒號(hào)寫下一段Java代碼——
String[] strings = {"Please", "sort", "the", "strings", "in", "REVERSE", "order"};
Arrays.sort(strings, new Comparator<String>() {
public int compare(String a, String b){ return -a.compareToIgnoreCase(b); }
});
引號(hào)很快讀懂了代碼:“這是將字符串組不區(qū)分大小寫地逆序排列。其中Comparator的匿名類實(shí)現(xiàn)了callback,因?yàn)樗姆椒?/span>compare是在類庫中被調(diào)用的。”
“此處callback的好處是顯而易見的——它使得Arrays.sort不再局限于自然排序,允許用戶自行定制排序規(guī)則,大大提高了算法的重用性。”冒號(hào)說著將幻燈片又翻到前頁,“回頭再看win32窗口程序的例子,其中第三步消息循環(huán)那段代碼不依賴應(yīng)用程序代碼,完全可以提煉出來作為library的一部分。事實(shí)上,在Visual C++里這段代碼就‘下放’到MFC類庫中去了。假設(shè)窗口過程由應(yīng)用程序直接調(diào)用,那么消息循環(huán)中的代碼將不再具有獨(dú)立性,無法作為公因子分解出來。”
嘆號(hào)塊壘頓消,暢然無比:“終于搞清那個(gè)怪異的窗口過程了!每個(gè)窗口在創(chuàng)建時(shí)就攜帶了一個(gè)callback,以后每當(dāng)系統(tǒng)偵查到事件,都能輕易地從事發(fā)窗口身上找到它的callback,然后調(diào)用它以響應(yīng)事件。”
“這等于將偵查事件與響應(yīng)事件兩項(xiàng)任務(wù)進(jìn)行了正交分解,降低了軟件的耦合度和復(fù)雜度。”句號(hào)言猶未盡,又加了一句,“就像剛才,引號(hào)負(fù)責(zé)偵查事件——警戒,逗號(hào)負(fù)責(zé)響應(yīng)事件——警醒。想法很好,可惜配合不夠默契,還是給人逮住了。”
逗、引二人大窘,余者大笑。
“仔細(xì)比較,以上兩個(gè)callback的用法還是稍有不同的。在字符串組排序中,callback在作為參數(shù)傳入底層的函數(shù)后,很快就在該函數(shù)體中被調(diào)用;在窗口程序中,callback則先被儲(chǔ)存起來,至于何時(shí)被調(diào)用完全是未定之?dāng)?shù)。用一句話概括:前者屬同步(synchronous)回調(diào),后者屬異步(asynchronous)回調(diào)。它們都使調(diào)用者不再依賴被調(diào)者,將二者從代碼上解耦,異步調(diào)用更將二者從時(shí)間上解耦。”冒號(hào)顯示出一副新圖——

“圖中處于底層的軟件平臺(tái)是在win32 API的基礎(chǔ)上的改進(jìn)。不僅把主循環(huán)從應(yīng)用程序中沉淀下來,而且將儲(chǔ)存callback的過程封裝在一個(gè)注冊(cè)函數(shù)中,使得應(yīng)用程序代碼變得更簡(jiǎn)潔、健壯。同時(shí)我們看到,整個(gè)流程的控制權(quán)已經(jīng)從應(yīng)用程序的主程序轉(zhuǎn)移到底層平臺(tái)的主循環(huán)中,符合好萊塢原則。”冒號(hào)。
逗號(hào)好奇地問:“什么是好萊塢原則?”
“don't call us, we'll call you.”冒號(hào)難得甩出一句洋文,“我很想畫蛇添足地在末尾加上單詞‘back’,這樣更容易理解callback的含義:‘call you back’。此話的背景大約是這樣的:一個(gè)藝人要想演出,需與好萊塢的經(jīng)紀(jì)公司聯(lián)系。由于幻想一朝成名的人太多,經(jīng)紀(jì)人總是牛氣十足,他們的口頭禪是:‘別打電話給我們,留下你的電話,有活干我們會(huì)打給你的’。”
引號(hào)認(rèn)真地解析:“好萊塢經(jīng)紀(jì)公司相當(dāng)于一個(gè)背后運(yùn)作的軟件平臺(tái),藝人相當(dāng)于一個(gè)callback,‘留下你的電話’就是注冊(cè)callback,‘我們會(huì)打給你的’就是異步調(diào)用callback。”
冒號(hào)接著補(bǔ)充:“‘別打電話給我們’意味著經(jīng)紀(jì)公司處于主導(dǎo)地位,藝人們處于受控狀態(tài),這便是控制反轉(zhuǎn)(Inversion of Control,簡(jiǎn)稱IoC)。”
問號(hào)聽著耳熟:“控制反轉(zhuǎn)?第一課談到框架時(shí)似乎提到過。”
“沒錯(cuò),正是它!”冒號(hào)談興愈濃,“一般library中用到callback只是局部的控制反轉(zhuǎn),而framework將IoC機(jī)制用到全局。程序員犧牲了對(duì)應(yīng)用程序流程的主導(dǎo)權(quán),換來的是更簡(jiǎn)潔的代碼和更高的生產(chǎn)效率。如果將編程譬比命題作文,不用framework的程序是一張可以自由寫作的白紙,library是作文素材庫;采用framework的程序是一篇成型的作文,作者只需填寫空白的詞語和段落即可。”
嘆號(hào)為之一嘆:“唉,編程序變成了做填空題,真沒勁! ”
“那你就多努力,爭(zhēng)取以后出填空題吧。”冒號(hào)笑著鼓勵(lì)他,“控制反轉(zhuǎn)不僅增強(qiáng)了framework在代碼和設(shè)計(jì)上的重用性,還極大地提高了framework的可擴(kuò)展性。這是因?yàn)?/span>framework的內(nèi)部運(yùn)轉(zhuǎn)機(jī)制雖是封閉的,但也開放了不少與外部相連的擴(kuò)展接口點(diǎn),類似插件(plugin)體系。如下圖所示——”

引號(hào)聯(lián)想到另一個(gè)名詞:“我知道有個(gè)依賴反轉(zhuǎn),與控制反轉(zhuǎn)是一回事嗎?”
冒號(hào)簡(jiǎn)答:“雖然不少人把它們看成同義詞,但依賴反轉(zhuǎn)原則(Dependency-Inversion Principle,簡(jiǎn)稱DIP)更加具體——高層模塊不應(yīng)依賴底層模塊,它們應(yīng)依賴抽象;抽象不應(yīng)依賴細(xì)節(jié),細(xì)節(jié)應(yīng)依賴抽象。經(jīng)常相提并論的還有依賴注射(Dependency Injection,簡(jiǎn)稱DI)——?jiǎng)討B(tài)地為一個(gè)軟件組件提供外部依賴。由于時(shí)間關(guān)系,不再詳加介紹。有一點(diǎn)可以看出,它們的主題是控制與依賴,目的是解耦,方法是反轉(zhuǎn),而實(shí)現(xiàn)這一切的關(guān)鍵是抽象接口。”
“為什么說是抽象接口而不是前面所說的回調(diào)函數(shù)?”打過瞌睡的逗號(hào)現(xiàn)在似乎變得特別清醒。
冒號(hào)予以說明:“回調(diào)函數(shù)的提法較為古老,多出現(xiàn)于過程式編程,抽象接口是更現(xiàn)代、更OO的說法。另外從字面上看,‘回調(diào)’強(qiáng)調(diào)的是行為方式——底層反調(diào)高層,而‘抽象接口’強(qiáng)調(diào)的是實(shí)現(xiàn)方式——正是由于接口具有抽象性,底層才能在調(diào)用它時(shí)無需慮及高層的具體細(xì)節(jié),從而實(shí)現(xiàn)控制反轉(zhuǎn)。”
眾人細(xì)細(xì)品味著冒號(hào)的這番話。
問號(hào)忽然驚覺:“我們是不是跑題了?本來是談事件驅(qū)動(dòng)式編程的,結(jié)果從callback談到控制反轉(zhuǎn),再到框架,現(xiàn)在又說起了抽象接口。”
“事物是普遍聯(lián)系的嘛。”冒號(hào)扯了句哲學(xué)套話,“不諳熟callback和IoC機(jī)制,就不可能真正領(lǐng)會(huì)事件驅(qū)動(dòng)式編程的精髓。不過,也該回到中心主題了。我們通過win32 API用四步實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的窗口程序,與事件直接相關(guān)的有三步:實(shí)現(xiàn)事件處理器(event handler)或事件監(jiān)聽器(event listener);注冊(cè)事件處理器;實(shí)現(xiàn)事件循環(huán)(event loop)。具體上,事件處理器負(fù)責(zé)處理事件,經(jīng)注冊(cè)方能在事發(fā)時(shí)收到通知;事件循環(huán)負(fù)責(zé)偵查事件、預(yù)處理事件、管理事件隊(duì)列和分派事件等,無事時(shí)默默等待,有事時(shí)立即響應(yīng),生命不息工作不止。在整個(gè)事件機(jī)制中,主循環(huán)好比心臟,事件處理器好比大腦,是最重要的兩類模塊。”
句號(hào)指出:“在支持事件驅(qū)動(dòng)的開發(fā)環(huán)境中,主循環(huán)是現(xiàn)成的。許多IDE的圖形編輯器在程序員點(diǎn)擊控件后,還能自動(dòng)生成事件處理器的骨架代碼,連注冊(cè)的步驟也免除了。”
冒號(hào)提醒他:“并不是總有這樣的好事,要知道事件驅(qū)動(dòng)式并不局限于GUI應(yīng)用,支持事件驅(qū)動(dòng)的開發(fā)環(huán)境也未必唾手可得。程序員有時(shí)必須自行設(shè)計(jì)整個(gè)事件系統(tǒng),他需要決定:采用事件驅(qū)動(dòng)式是否合適?如果合適,如何設(shè)計(jì)事件機(jī)制?其中包括事件定義、事件觸發(fā)、事件偵查、事件轉(zhuǎn)化、事件合并、事件調(diào)度、事件傳播、事件處理、事件連帶(event cascade)[5]等等一系列問題。”
嘆號(hào)扮著苦相說:“我的腦袋就是一個(gè)事件監(jiān)聽器,在聽到要面臨這么多的事件后,迅速作出反應(yīng)——大了一圈。”
眾皆彎腰捧腹。
“腦袋能變大是件好事啊,說明它伸縮性強(qiáng),相信用它來編的程序也是一樣。”冒號(hào)打著哈哈,“事件驅(qū)動(dòng)式的程序可伸縮性就很強(qiáng),知道為什么嗎?”
逗號(hào)隨口說道:“不是因?yàn)槔没卣{(diào)函數(shù)實(shí)現(xiàn)了控制反轉(zhuǎn)嗎?”
“非也非也。”冒號(hào)文縐縐地說,“軟件的可伸縮性(scalability)一般指應(yīng)對(duì)工作量增長的能力,多出于性能方面的考量。而控制反轉(zhuǎn)的主要作用是降低模塊之間的依賴性,從而降低模塊的耦合度和復(fù)雜度,提高軟件的可重用性、柔韌性和可擴(kuò)展性,但對(duì)可伸縮性并無太大幫助。我們已經(jīng)看到,控制反轉(zhuǎn)導(dǎo)致了事件驅(qū)動(dòng)式編程的被動(dòng)性(passivity)。此外,事件驅(qū)動(dòng)式還具有異步性(asynchrony)的特征,這是由事件的不可預(yù)測(cè)性與隨機(jī)性決定的。如果一個(gè)應(yīng)用中存在一些該類特質(zhì)的因素,比如頻繁出現(xiàn)堵塞呼叫(blocking call),不妨考慮將其包裝為事件。”
問號(hào)打岔道:“什么是堵塞呼叫?”
冒號(hào)作了個(gè)比方:“在高速公路上一輛車突然出故障停在路途,急調(diào)維修人員。如果現(xiàn)場(chǎng)修理,在修好之前所在車道是堵塞的,后面車輛無法通行。類似地,在程序中一些函數(shù)需要等待某些數(shù)據(jù)而不能立即返回[6],從而堵塞整個(gè)進(jìn)程。”
引號(hào)道出常識(shí):“顯然更可取的修車做法是:先把車拖到路邊,修完后向其他車輛發(fā)出信號(hào),以便重回車道。”
冒號(hào)趁熱打鐵:“同理,我們可以讓堵塞呼叫暫時(shí)脫離主進(jìn)程,事成之后再利用事件機(jī)制申請(qǐng)重返原進(jìn)程。相比第一種同步流程式的方案,這種異步事件式將連續(xù)的進(jìn)程中獨(dú)立且耗時(shí)的部分抽取出來,從而減少隨機(jī)因素造成的資源浪費(fèi),提高系統(tǒng)的性能和可伸縮性。”
問號(hào)聽得仔細(xì):“為什么抽取的部分是‘獨(dú)立且耗時(shí)’,而不是‘隨機(jī)且耗時(shí)’?”
“問得好!”冒號(hào)很欣賞他嚴(yán)謹(jǐn)?shù)膶W(xué)風(fēng),“再拿修車來說,第二種方案之所以可行有兩方面原因:一是修車耗時(shí),二是修車獨(dú)立。所謂獨(dú)立又有兩層含義:與車道獨(dú)立——修車時(shí)不必占用車道;與后車獨(dú)立——后面車輛不必恭候該車。如果一分鐘內(nèi)能修好,或者路邊沒有足夠空位,再或者后面車輛是故障車的隨行車,那么拖車方案均不成立。大家可以自己類比堵塞呼叫的情形,我就不再饒舌了。總之,獨(dú)立是異步的前提,耗時(shí)是異步的理由。至于隨機(jī)嘛,只是副產(chǎn)品,一個(gè)獨(dú)立且耗時(shí)的子過程,通常結(jié)束時(shí)間也是不可預(yù)期的。”
眼見天色已晚,冒號(hào)趕忙換上最后一頁幻燈片——

“上圖為一個(gè)典型的事件驅(qū)動(dòng)式模型。事件處理器事先在關(guān)注的事件源上注冊(cè),后者不定期地發(fā)表事件對(duì)象,經(jīng)過事件管理器的轉(zhuǎn)化(translate)、合并(coalesce)、排隊(duì)(enqueue)、分派(dispatch)等集中處理后,事件處理器接收到事件并對(duì)其進(jìn)行相應(yīng)處理。請(qǐng)注意事件處理器隨時(shí)可以注冊(cè)或注銷事件源,意味著二者之間的關(guān)系是動(dòng)態(tài)建立和解除的。”冒號(hào)在幻燈屏上指指點(diǎn)點(diǎn),“通過事件機(jī)制,事件源與事件處理器之間建立了松耦合的多對(duì)多關(guān)系:一個(gè)事件源可以有多個(gè)處理器,一個(gè)處理器可以監(jiān)聽多個(gè)事件源。再換個(gè)角度,把事件處理器視為服務(wù)方,事件源視為客戶方,便是一個(gè)client-server模式。每個(gè)服務(wù)方與其客戶方之間的會(huì)話(session)是異步的,即在處理完一個(gè)客戶的請(qǐng)求后不必等待下一請(qǐng)求,隨時(shí)可切換(switch)到對(duì)其他客戶的服務(wù)。更有甚者,事件處理器也能產(chǎn)生事件,實(shí)現(xiàn)處理器接口的事件源也能處理事件,它們可以角色換位,于是又演化為peer-to-peer模式。”
嘆號(hào)抱怨:“有點(diǎn)眼花繚亂了。”
為濕潤枯燥的理論,冒號(hào)再次舉例:“你們不是很喜歡在QQ上聊天嗎?QQ服務(wù)器是事件管理器,每個(gè)聊天者既是事件源又是事件處理器,這正是事件驅(qū)動(dòng)式的P2P模式啊[7]。此外,聊天時(shí)不等對(duì)方回答,就可與另一網(wǎng)友交談,這就是會(huì)話切換帶來的異步效果。不過同樣是聊天,改用電話就稍有不同了。”
冒號(hào)掃了 眾人一眼,果見有人皺起了眉頭。
“當(dāng)你正用座機(jī)通話時(shí),手機(jī)響了。你會(huì)怎么做?”冒號(hào)提示。
逗號(hào)本能地回答:“要么掛掉電話再接手機(jī),要么讓打手機(jī)的人遲些打來。”
句號(hào)聽出了門道:“這說明電話的通話過程是同步而非異步的,原因是打電話雙方的交流是連貫的、非堵塞式的(non-blocking),與QQ聊天正好相反。”
冒號(hào)點(diǎn)頭稱許。
雖然早已過了下課時(shí)間,引號(hào)仍是好學(xué)不倦:“我覺得觀察者模式與事件驅(qū)動(dòng)式很像啊。”
“你開始不是還舉了訂閱雜志和RSS的例子嗎?出版/訂閱(publish-subscribe)模式[8]正是觀察者(observer)模式的別名,一方面可看作簡(jiǎn)化或退化的事件驅(qū)動(dòng)式,另一方面可看作事件驅(qū)動(dòng)式的核心思想。該模式省略了事件管理器部分,由事件源直接調(diào)用事件處理器的接口。這樣更加簡(jiǎn)明易用,但威力有所削弱,缺少事件管理、事件連帶等機(jī)制。著名的MVC(Model-View-Controller)模型正是它的一個(gè)應(yīng)用。”冒號(hào)長舒了一口氣,準(zhǔn)備收工,“事件驅(qū)動(dòng)式的應(yīng)用極廣,變化極多,還涉及到框架、設(shè)計(jì)模式、架構(gòu)、以及其他的編程范式,本身也可作為一種架構(gòu)模型。今天我們僅僅是蜻蜓點(diǎn)水,更深入更具體的內(nèi)容只能留后探討了。時(shí)候不早,你們也該餓了,趕快回家吧!范式可不能當(dāng)飯吃哦。”
眾人笑作鳥獸散。
,插語
[1] 許多基于事件驅(qū)動(dòng)的系統(tǒng)都提供了createEvent之類的API,授權(quán)編程者自行產(chǎn)生事件。
[2] 更準(zhǔn)確地說,Windows先把所有的硬件事件存入系統(tǒng)消息隊(duì)列(system message queue),然后再放入應(yīng)用程序消息隊(duì)列(application message queue)。
[3] 比如可以這樣從msg中得到窗口過程: (WNDPROC)GetWindowLong(msg.hwnd, GWL_WNDPROC)。
[4] 后面的論述同樣適用于其他形式的軟件分層結(jié)構(gòu)。
[5] 指事件處理器在處理過程中又產(chǎn)生新的事件,從而再次觸發(fā)事件處理器。
[6] 比如套接字(socket)中的accept函數(shù)。
[7] 真正的P2P網(wǎng)絡(luò)是不需要中心服務(wù)器的,此處P2P指聊天雙方是不分主客的對(duì)等關(guān)系。
[8] 有人將出版-訂閱模式視為事件驅(qū)動(dòng)設(shè)計(jì)的同義詞,這是有道理的:在實(shí)際生活中,處于出版商與訂閱者之間的郵局可作為事件管理器。
。總結(jié)
- 事件是程序中令人關(guān)注的信息狀態(tài)上變化。在基于事件驅(qū)動(dòng)的系統(tǒng)中,事件包括內(nèi)建事件與用戶自定義事件,其中內(nèi)建事件又分為底層事件和語義事件。此外,事件還有自然事件與合成事件之分。
- Callback指能作為參數(shù)傳遞的函數(shù)或代碼,它允許底層模塊調(diào)用高層模塊,使調(diào)用者與被調(diào)者從代碼上解耦。異步callback在傳入后并不立即被調(diào)用,使調(diào)用者與被調(diào)者從時(shí)間上解耦。
- 控制反轉(zhuǎn)一般通過callback來實(shí)現(xiàn),其目的是降低模塊之間的依賴性,從而降低模塊的耦合度和復(fù)雜度。
- 在框架設(shè)計(jì)中,控制反轉(zhuǎn)增強(qiáng)了軟件的可重用性、柔韌性和可擴(kuò)展性,減少了用戶的負(fù)擔(dān),簡(jiǎn)化了用戶的代碼。
- 控制反轉(zhuǎn)、依賴反轉(zhuǎn)原則和依賴注射是近義詞,它們的主題是控制與依賴,目的是解耦,方法是反轉(zhuǎn),而實(shí)現(xiàn)這一切的關(guān)鍵是抽象接口。
- 事件驅(qū)動(dòng)式編程的三個(gè)步驟:實(shí)現(xiàn)事件處理器;注冊(cè)事件處理器;實(shí)現(xiàn)事件循環(huán)。
- 異步過程在主程序中以非堵塞的機(jī)制運(yùn)行,即主程序不必等待該過程的返回就能繼續(xù)下一步。異步機(jī)制能減少隨機(jī)因素造成的資源浪費(fèi),提高系統(tǒng)的性能和可伸縮性。
- 獨(dú)立是異步的前提,耗時(shí)是異步的理由。
- 事件驅(qū)動(dòng)式最重要的兩個(gè)特征是被動(dòng)性和異步性。被動(dòng)性來自控制反轉(zhuǎn),異步性來自會(huì)話切換。
- 觀察者模式又名出版/訂閱模式,既是事件驅(qū)動(dòng)式的簡(jiǎn)化,也是事件驅(qū)動(dòng)式的核心思想。MVC模型是觀察者模式的一個(gè)應(yīng)用。
“”參考
[1] Wikipedia.Event-driven programming.http://en.wikipedia.org/wiki/Event-driven
[2] Wikipedia.Callback (computer science).http://en.wikipedia.org/wiki/Callback_(computer_science)
[3] Charles Petzold.Programming Windows,5th ed..Redmond:Microsoft Press,1999.41-70
[4] Robert C. Martin.Agile Software Development: Principles, Patterns, and Practices(影印版).北京:中國電力出版社,2003.127-134
[5] Martin Fowler.Inversion of Control Containers and the Dependency Injection pattern.http://martinfowler.com/articles/injection.html
[6] Erich Gamma,Richard Helm,Ralph Johnson,John Vlissides.Design Patterns: Elements of Reusable Object-Oriented Software.Boston:Addison-Wesley,1994.293-299
課后思考
- 了解C++中的STL和Java中的 Collections Framework。
- 當(dāng)你成功構(gòu)想地并實(shí)現(xiàn)了一個(gè)算法,是否考慮過利用泛型編程來擴(kuò)大其適用范圍以提高其重用性?
- 當(dāng)你發(fā)覺幾個(gè)模塊中有類似的算法,是否考慮過利用泛型思想進(jìn)行重構(gòu)?
- 當(dāng)你發(fā)覺程序中有大量類似的代碼,是否考慮過用產(chǎn)生式編程來自動(dòng)生成它們?
- 試著利用編譯器生成器(如ANTLR)自定義一種DSL,并用它來解決問題。
- 你采用過AOP嗎?它有哪些優(yōu)缺點(diǎn)?
- 如何合理地抽象出系統(tǒng)的橫切關(guān)注點(diǎn)?
- 請(qǐng)對(duì)比流程驅(qū)動(dòng)式編程與事件驅(qū)動(dòng)式編程之間的差異,它們各自適合哪些應(yīng)用?
- 你編寫的代碼是否有足夠的靈活性和可擴(kuò)展性?能否利用控制反轉(zhuǎn)原理?
- 你的程序中是如何處理堵塞呼叫的?是否考慮過引入異步機(jī)制?