日历

September 2009
M T W T F S S
« Aug   Oct »
 123456
78910111213
14151617181920
21222324252627
282930  

冒号课堂§3.4:事件驱动

冒号课堂

第三课 常用范式(4)

3.4 事件驱动——有事我叫你,没事别烦我

郑晖

摘要

事件驱动式编程简谈


劳心者治人,劳力者治于人 

《孟子•滕文公上》

!预览

  • 它们(同步回调和异步回调)都使调用者不再依赖被调者,将二者从代码上解耦,异步调用更将二者从时间上解耦

  • 它们(控制反转、依赖反转和依赖注射)的主题是控制与依赖,目的是解耦,方法是反转,而实现这一切的关键是抽象接口

  • “回调”强调的是行为方式——低层反调高层,而“抽象接口”强调的是实现方式——正是由于接口具有抽象性,低层才能在调用它时无需虑及高层的具体细节,从而实现控制反转

  • 控制反转导致了事件驱动式编程的被动性

  • 事件驱动式还具有异步性的特征,这是由事件的不可预测性与随机性决定的

  • 独立是异步的前提,耗时是异步的理由

  • 发行/订阅模式正是观察者模式的别名,一方面可看作简化或退化的事件驱动式,另一方面可看作事件驱动式的核心思想

?提问

  • 什么是事件?有哪些不同类型的事件?

  • 什么是回调函数?什么是异步同调?它们有什么用处?

  • 控制反转的目的是什么?它是如何实现的?在框架设计中起什么作用?

  • 控制反转、依赖反转原则和依赖注射的共同点是什么?

  • 事件驱动式编程有哪些关键步骤?

  • 异步过程特点和作用是什么?

  • 事件驱动式编程最重要的特征是什么?它们是如何实现的?

  • 事件驱动式与观察者模式、MVC架构有何关系?

:讲解

逗号渐觉睡虫上脑,开始闭目点头。正神游之际,忽觉腰间一阵酥麻。惺眼微睁,原是被引号的胳膊肘给捅的,顿时警醒。抬头见讲台上的老冒正目光灼灼地盯着自己,不禁脸颊微烫,嗫嚅道:“不好意思,昨晚睡得太晚了。”

冒号却不以为意:“正愁找不到新话题呢,你倒启发我了。话说课堂上睡觉大抵有三种方式——”

话音未落,有人已笑不自禁。

“第一种是警觉式:想睡可又担心被老师发现,不时睁眼查看周围的变化。同时双耳保持警戒,一有异动立刻挺直身板。”冒号有板有眼地形容,“第二种是宽心式:俯桌酣睡,如处无人之境。境界至高者或可雷打不动,或可鼾声如雷。”

“总之是很雷人。”叹号的网络新语再度引发笑声。

冒号继续分析:“第三种是托付式:请人放哨,非急勿扰。遂再无顾忌,大可封目垂耳,安心入眠。请问你们乐意采用哪种方式?”

“第一种方式睡不踏实,不得已而为之。敢用第二种方式的人多半没心没肺,估计IT人都达不到那种境界。只要有同伴在身旁,我想大家都会选第三种方式的。”句号的回答获得一致认同。

冒号续问:“好,抛开第二种方式不谈,为什么第三种要比第一种优越呢?”

句号回答:“犯困者既要打盹又要警戒,必然苦不堪言。如果把警戒的任务委托同伴,两人分工合作,自然愉快得多。”

冒号再问:“他们是如何合作的呢?”

“放哨者一旦发现有情况,立即通知犯困者采取行动——睁眼坐直,作认真听讲状。”句号说得是绘声绘色。

除了两位当事人略显尴尬外,其他人均乐不可支。

眼见时机成熟,冒号不再兜圈:“采用警觉式者主动去轮询(polling),行为取决于自身的观察判断,是流程驱动的,符合常规的流程驱动式编程(Flow-Driven Programming)的模式。采用托付式者被动等通知(notification),行为取决于外来的突发事件,是事件驱动的,符合事件驱动式编程(Event-Driven Programming,简称EDP)的模式。下面我们就来说说这种编程范式。”

逗号瓮声瓮气道:“没想到打瞌睡打出了个范式。”

冒号瞥了他一眼,继续说下去:“为完成一样事,既可以采用流程驱动式,也可以采用事件驱动式。这样的例子在生活中可谓俯拾即是,刚才逗号同学为大家现场示范了一个,谁还能举出其他范例?”

叹号抢先举例:“与客户打交道,推销员主动打电话或登门拜访,他的工作是流程驱动的;接线员坐等电话,他的工作是事件驱动的。”

问号也说:“同样是交通工具,公共汽车主要是流程驱动的,它的路线已预先设定;出租车主要是事件驱动的,它的路线基本上由随机搭载的乘客所决定。”

引号以个人经验作例:“购买喜爱的杂志可以选择频繁光顾报刊亭,也可以选择一次性订阅。浏览关注的新闻网站或博客,可以直接访问站点,也可以订阅相应的RSS。主动检查所关心的内容是否更新是流程驱动的,用订阅的方式是事件驱动的。”

句号回到本行:“Windows下的许多工作既可以在DOS下用批处理程序实现,也可以在图形界面下完成。前者不需人工干预,显然是流程驱动的;后者毫无疑问是事件驱动的。”

“看来你们对这种范式很熟悉嘛。不过,它原理虽简单,威力却无穷。看似一招,实则暗藏百式,甚可幻化千招。个中精妙之处,断非一时可以尽述。”冒号不知不觉中又走进了武侠的世界。

众人听了,暗疑老冒有些言过其实。

冒号正式入题:“首当其冲的问题是:何谓事件?通俗地说,它是已经发生的某种令人关注的事情。在软件中,它一般表现为一个程序的某些信息状态上的变化。基于事件驱动的系统一般提供两类的内建事件(built-in event):一类是底层事件(low-level event)或称原生事件(native event),在用户图形界面(GUI)系统中这类事件直接由鼠标、键盘等硬件设备触发;一类是语义事件(semantic event),一般代表用户的行为逻辑,是若干底层事件的组合。比如鼠标拖放(drag-and-drop)多表示移动被拖放的对象,由鼠标按下、鼠标移动和鼠标释放三个底层事件组成。”

问号推想:“编程人员应该还能创造新的事件类型吧?”

“那是当然。”冒号点点头,“还有一类用户自定义事件(user-defined event)。它们可以是在原有的内建事件的基础上进行的包装,也可以是纯粹的虚拟事件(virtual event)。除此之外,编程者不但能定义事件,还能产生事件。虽然大部分事件是由外界激发的自然事件(natural event),但有时程序员需要主动激发一些事件,比如模拟用户鼠标点击或键盘输入等,这类事件被称为合成事件(synthetic event)[1]。这些都进一步丰富完善了事件体系和事件机制,使得事件驱动式编程更具渗透性。”

叹号嘟哝了一句:“看来这里边还有点名堂。”

“名堂多着呢!”冒号回应,“事件固然是事件驱动式编程的核心概念,但一个编程范式的独特之处绝不仅仅是一些概念,更重要的是建立于这些概念之上的思维模式。为了了解这种范式与众不同的特点,我们先看看如何利用win32的API在windows下创建一个简单的窗口——”

/** 一个win32窗口程序 */
…WinMain(...) // windows应用程序的主函数
{
    // 第一步——注册窗口类别
    ...;
    windowClass.lpfnWndProc = WndProc; // 指定该类窗口的回调函数
    windowClass.lpszClassName = windowClassName; // 指定该类窗口的名字
    RegisterClassEx(&windowClass);

    //第二步——创建一个上述类别的窗口
    CreateWindowEx(…, windowClassName, ...);
    …;

    //  第三步——消息循环
    while (GetMessage(&msg, NULL, 0, 0)  > 0) // 获取消息
    {
        TranslateMessage(&msg); // 翻译键盘消息
        DispatchMessage(&msg);  // 分派消息
    }
}

// 第四步——窗口过程(处理消息)
…WndProc(…, msg,...)
{
    switch (msg)
    {
        case WM_SIZE:   …;   // 用户改变窗口尺寸
        case WM_MOVE: …; // 用户移动窗口
        case WM_CLOSE: …; // 用户关闭窗口
        …;
    }
}

“没有选用Java、Visual C++、C#、VB或者Delphi来实现窗口,是因为它们高度的封装和强大的IDE掩盖了部分事件机制。如果你们对win32 API不太熟悉,没有关系。为了减少语言和API上的障碍,同时突出重点,这里最大限度地省略了次要的过程和参数等,仅保留脉络主干。”冒号解释,“从中看出到,创建一个能响应用户操作的win32窗口共分四步:注册窗口类别、创建窗口、消息循环和窗口过程。”

问号对概念很敏感:“消息与事件是一回事吗?”

“严格说来它们不是一回事,但如果你不想深究,不加区分也无大碍。概略地说,消息是Windows内部最基本的通讯方式,事件需要通过消息来传递,是消息的主要来源。每当用户触发一个事件,如移动鼠标或敲击键盘,系统都会将其转化为消息并放入相应程序的消息队列(message queue)中[2]。”冒号解答着,“明白了这一点,上面的代码就不难理解了——在消息循环中,程序通过GetMessage不断地从消息队列中获取消息,经过TranslateMessage预处理后再通过DispatchMessage将消息送交窗口过程WndProc处理。”

逗号琢磨了一会,不解地问:“窗口过程应该是在分派消息时被调用的,但我怎么想不出DispatchMessage是如何联系到WndProc的?”

冒号为其解惑:“DispatchMessage的消息参数含有事发窗口的句柄(handle),从而可以得到窗口过程WndProc[3]。至于窗口与窗口过程之间是如何建立联系的,回看前面两步就一目了然了:当初在创建窗口时指明了窗口类别名windowClassName,而窗口类别windowClass又绑定了窗口过程。”

叹号有点纳闷:“干嘛要绕这么大的弯子,直接调用WndProc不就得了?”

“对于这个简单的程序来说,的确区别不大。但假如再增添其他菜单、按钮、文本框之类的控件,每个控件都可绑定自己的窗口过程,那么到底该调用哪个才对呢?”冒号反问。

叹号虽有所悟,但仍有心结:“总觉得窗口过程的用法有些怪怪的。”

冒号一敲桌案:“没错!怪就怪在编程者自己写了一个应用层的函数,却不直接调用它,而是通过库函数间接调用。这类函数有个专用名称:回调函数(callback)。”

引号忍不住插话:“回调函数我知道,在C和C++中就是函数指针嘛。”

“确切地说,函数指针是C和C++用来实现callback的一种方式。此外,抽象类(abstract class)、接口(interface)、C++中的泛型函子(generic functor)和C#中的委托(delegate)都可实现callback。我们先图解一下回调机制。”冒号调出一张图示——

图3-4. 普通函数与回调函数的对比

普通函数与回调函数的对比

“如果我们把系统划分为两层[4]:低层的函数库和高层的应用程序。同样作为主函数的辅助函数,左图中的普通函数直接被主函数调用,然而右图中的回调函数却是通过库函数间接被主函数调用的。”冒号的手影在幻灯下上下翻飞。

句号点出要害:“一般都是高层代码调用低层代码,callback反其道而行之,因此显得与众不同。”

“所言极是。一方面,在软件模块分层中,低层模块为高层模块提供服务,并且不能依赖高层模块,以保证其可重用性;另一方面,通常被调者(callee)为调用者(caller)提供服务,调用者依赖被调者。两相结合,决定了低层模块多为被调者,高层模块多为调用者。但这种惯例并不总是合适的——低层模块为了追求更强的普适性和可扩展性,有时也有调用高层模块的需求,于是便邀callback前来相助。我们看一个简单的例子。”冒号写下一段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); }
    });

引号很快读懂了代码:“这是将字符串组不区分大小写地逆序排列。其中Comparator的匿名类实现了callback,因为它的方法compare是在类库中被调用的。”

“此处callback的好处是显而易见的——它使得Arrays.sort不再局限于自然排序,允许用户自行定制排序规则,大大提高了算法的重用性。”冒号说着将幻灯片又翻到前页,“回头再看win32窗口程序的例子,其中第三步消息循环那段代码不依赖应用程序代码,完全可以提炼出来作为library的一部分。事实上,在Visual C++里这段代码就‘下放’到MFC类库中去了。假设窗口过程由应用程序直接调用,那么消息循环中的代码将不再具有独立性,无法作为公因子分解出来。”

叹号块垒顿消,畅然无比:“终于搞清那个怪异的窗口过程了!每个窗口在创建时就携带了一个callback,以后每当系统侦查到事件,都能轻易地从事发窗口身上找到它的callback,然后调用它以响应事件。”

“这等于将侦查事件与响应事件两项任务进行了正交分解,降低了软件的耦合度和复杂度。”句号言犹未尽,又加了一句,“就像刚才,引号负责侦查事件——警戒,逗号负责响应事件——警醒。想法很好,可惜配合不够默契,还是给人逮住了。”

逗、引二人大窘,余者大笑。

“仔细比较,以上两个callback的用法还是稍有不同的。在字符串组排序中,callback在作为参数传入低层的函数后,很快就在该函数体中被调用;在窗口程序中,callback则先被储存起来,至于何时被调用完全是未定之数。用一句话概括:前者属同步(synchronous)回调,后者属异步(asynchronous)回调。它们都使调用者不再依赖被调者,将二者从代码上解耦,异步调用更将二者从时间上解耦。”冒号显示出一副新图——

图3-5. 异步回调

异步回调

“图中处于低层的软件平台是在win32 API的基础上的改进。不仅把主循环从应用程序中沉淀下来,而且将储存callback的过程封装在一个注册函数中,使得应用程序代码变得更简洁、健壮。同时我们看到,整个流程的控制权已经从应用程序的主程序转移到底层平台的主循环中,符合好莱坞原则。”冒号。

逗号好奇地问:“什么是好莱坞原则?”

“don’t call us, we’ll call you.”冒号难得甩出一句洋文,“我很想画蛇添足地在末尾加上单词‘back’,这样更容易理解callback的含义:‘call you back’。此话的背景大约是这样的:一个艺人要想演出,需与好莱坞的经纪公司联系。由于幻想一朝成名的人太多,经纪人总是牛气十足,他们的口头禅是:‘别打电话给我们,留下你的电话,有活干我们会打给你的’。”

引号认真地解析:“好莱坞经纪公司相当于一个背后运作的软件平台,艺人相当于一个callback,‘留下你的电话’就是注册callback,‘我们会打给你的’就是异步调用callback。”

冒号接着补充:“‘别打电话给我们’意味着经纪公司处于主导地位,艺人们处于受控状态,这便是控制反转(Inversion of Control,简称IoC)。”

问号听着耳熟:“控制反转?第一课谈到框架时似乎提到过。”

“没错,正是它!”冒号谈兴愈浓,“一般library中用到callback只是局部的控制反转,而framework将IoC机制用到全局。程序员牺牲了对应用程序流程的主导权,换来的是更简洁的代码和更高的生产效率。如果将编程譬比命题作文,不用framework的程序是一张可以自由写作的白纸,library是作文素材库;采用framework的程序是一篇成型的作文,作者只需填写空白的词语和段落即可。”

叹号为之一叹:“唉,编程序变成了做填空题,真没劲! ”

“那你就多努力,争取以后出填空题吧。”冒号笑着鼓励他,“控制反转不仅增强了framework在代码和设计上的重用性,还极大地提高了framework的可扩展性。这是因为framework的内部运转机制虽是封闭的,但也开放了不少与外部相连的扩展接口点,类似插件(plugin)体系。如下图所示——”

图3-6. 框架的IoC机制

框架的IoC机制

引号联想到另一个名词:“我知道有个依赖反转,与控制反转是一回事吗?”

冒号简答:“虽然不少人把它们看成同义词,但依赖反转原则(Dependency-Inversion Principle,简称DIP)更加具体——高层模块不应依赖低层模块,它们都应依赖抽象;抽象不应依赖细节,细节应依赖抽象。经常相提并论的还有依赖注射(Dependency Injection,简称DI)——动态地为一个软件组件提供外部依赖。由于时间关系,它们之间的区别容后再叙。有一点可以看出,它们的主题是控制与依赖,目的是解耦,方法是反转,而实现这一切的关键是抽象接口。”

“为什么说是抽象接口而不是前面所说的回调函数?”打过瞌睡的逗号现在似乎变得特别清醒。

冒号予以说明:“回调函数的提法较为古老,多出现于过程式编程,抽象接口是更现代、更OO的说法。另外从字面上看,‘回调’强调的是行为方式——低层反调高层,而‘抽象接口’强调的是实现方式——正是由于接口具有抽象性,低层才能在调用它时无需虑及高层的具体细节,从而实现控制反转。”

众人细细品味着冒号的这番话。

问号忽然惊觉:“我们是不是跑题了?本来是谈事件驱动式编程的,结果从callback谈到控制反转,再到框架,现在又说起了抽象接口。”

“事物是普遍联系的嘛。”冒号扯了句哲学套话,“不谙熟callback和IoC机制,就不可能真正领会事件驱动式编程的精髓。不过,也该回到中心主题了。我们通过win32 API用四步实现了一个简单的窗口程序,与事件直接相关的有三步:实现事件处理器(event handler)或事件监听器(event listener);注册事件处理器;实现事件循环(event loop)。具体上,事件处理器负责处理事件,经注册方能在事发时收到通知;事件循环负责侦查事件、预处理事件、管理事件队列和分派事件等,无事时默默等待,有事时立即响应,生命不息工作不止。在整个事件机制中,主循环好比心脏,事件处理器好比大脑,是最重要的两类模块。”

句号指出:“在支持事件驱动的开发环境中,主循环是现成的。许多IDE的图形编辑器在程序员点击控件后,还能自动生成事件处理器的骨架代码,连注册的步骤也免除了。”

冒号提醒他:“并不是总有这样的好事,要知道事件驱动式并不局限于GUI应用,支持事件驱动的开发环境也未必唾手可得。程序员有时必须自行设计整个事件系统,他需要决定:采用事件驱动式是否合适?如果合适,如何设计事件机制?其中包括事件定义、事件触发、事件侦查、事件转化、事件合并、事件调度、事件传播、事件处理、事件连带(event cascade)[5]等等一系列问题。”

逗号扮着苦相说:“我的脑袋就是一个事件监听器,在听到要面临这么多的事件后,迅速作出反应——大了一圈。”

众皆弯腰捧腹。

“脑袋能变大是件好事啊,说明它伸缩性强,相信用它来编的程序也是一样。”冒号打着哈哈,“事件驱动式的程序可伸缩性就很强,知道为什么吗?”

叹号随口说道:“不是因为利用回调函数实现了控制反转吗?”

“非也非也。”冒号文绉绉地说,“软件的可伸缩性(scalability)一般指从容应对工作量增长的能力,常与性能(performance)等指标一并被考量。而控制反转的主要作用是降低模块之间的依赖性,从而降低模块的耦合度和复杂度,提高软件的可重用性、柔韧性和可扩展性,但对可伸缩性并无太大帮助。我们已经看到,控制反转导致了事件驱动式编程的被动性(passivity)。此外,事件驱动式还具有异步性(asynchrony)的特征,这是由事件的不可预测性与随机性决定的。如果一个应用中存在一些该类特质的因素,比如频繁出现堵塞呼叫(blocking call),不妨考虑将其包装为事件。”

问号打岔道:“什么是堵塞呼叫?”

冒号作了个比方:“在高速公路上一辆车突然出故障停在路途,急调维修人员。如果现场修理,在修好之前所在车道是堵塞的,后面车辆无法通行。类似地,在程序中一些函数需要等待某些数据而不能立即返回[6],从而堵塞整个进程。”

引号道出常识:“显然更可取的修车做法是:先把车拖到路边,修完后向其他车辆发出信号,以便重回车道。”

冒号趁热打铁:“同理,我们可以让堵塞呼叫暂时脱离主进程,事成之后再利用事件机制申请重返原进程。相比第一种同步流程式的方案,这种异步事件式将连续的进程中独立且耗时的部分抽取出来,从而减少随机因素造成的资源浪费,提高系统的性能和可伸缩性。”

问号听得仔细:“为什么抽取的部分是‘独立且耗时’,而不是‘随机且耗时’?”

“问得好!”冒号很欣赏他严谨的学风,“再拿修车来说,第二种方案之所以可行有两方面原因:一是修车耗时,二是修车独立。所谓独立又有两层含义:与车道独立——修车时不必占用车道;与后车独立——后面车辆不必恭候该车。如果一分钟内能修好,或者路边没有足够空位,再或者后面车辆是故障车的随行车,那么拖车方案均不成立。大家可以自己类比堵塞呼叫的情形,我就不再饶舌了。总之,独立是异步的前提,耗时是异步的理由。至于随机嘛,只是副产品,一个独立且耗时的子过程,通常结束时间也是不可预期的。”

眼见天色已晚,冒号赶忙换上最后一页幻灯片——

图3-7. 事件驱动式模型

事件驱动式模型

“上图为一个典型的事件驱动式模型。事件处理器事先在关注的事件源上注册,后者不定期地发表事件对象,经过事件管理器的转化(translate)、合并(coalesce)、排队(enqueue)、分派(dispatch)等集中处理后,事件处理器接收到事件并对其进行相应处理。请注意事件处理器随时可以注册或注销事件源,意味着二者之间的关系是动态建立和解除的。”冒号在幻灯屏上指指点点,“通过事件机制,事件源与事件处理器之间建立了松耦合多对多关系:一个事件源可以有多个处理器,一个处理器可以监听多个事件源。再换个角度,把事件处理器视为服务方,事件源视为客户方,便是一个client-server模式。每个服务方与其客户方之间的会话(session)是异步的,即在处理完一个客户的请求后不必等待下一请求,随时可切换(switch)到对其他客户的服务。更有甚者,事件处理器也能产生事件,实现处理器接口的事件源也能处理事件,它们可以角色换位,于是又演化为peer-to-peer模式。”

叹号抱怨:“有点眼花缭乱了。”

为湿润枯燥的理论,冒号再次举例:“你们不是很喜欢在QQ上聊天吗?QQ服务器是事件管理器,每个聊天者既是事件源又是事件处理器,这正是事件驱动式的P2P模式啊[7]。此外,聊天时不等对方回答,就可与另一网友交谈,这就是会话切换带来的异步效果。不过同样是聊天,改用电话就稍有不同了。”

冒号扫了 众人一眼,果见有人皱起了眉头。

“当你正用座机通话时,手机响了。你会怎么做?”冒号提示。

逗号本能地回答:“要么挂掉电话再接手机,要么让打手机的人迟些打来。”

句号听出了门道:“这说明电话的通话过程是同步而非异步的,原因是打电话双方的交流是连贯的、非堵塞式的(non-blocking),与QQ聊天正好相反。”

冒号点头称许。

虽然早已过了下课时间,引号仍是好学不倦:“我觉得观察者模式与事件驱动式很像啊。”

“你开始不是还举了订阅杂志和RSS的例子吗?发行/订阅模式(publish-subscribe pattern)[8]正是观察者模式(observer pattern)的别名,一方面可看作简化或退化的事件驱动式,另一方面可看作事件驱动式的核心思想。该模式省略了事件管理器部分,由事件源直接调用事件处理器的接口。这样更加简明易用,但威力有所削弱,缺少事件管理、事件连带等机制。著名的MVC(Model-View-Controller)架构正是它在架构设计上的一个应用[9]。”冒号长舒了一口气,准备收工,“事件驱动式的应用极广,变化极多,还涉及到框架、设计模式、架构、以及其他的编程范式,本身也可作为一种架构模型。今天我们仅仅是蜻蜓点水,更深入更具体的内容只能留后探讨了。时候不早,你们也该饿了,赶快回家吧!范式可不能当饭吃哦。”

众人笑作鸟兽散。

,插语

  1. 许多基于事件驱动的系统都提供了createEvent之类的API,授权编程者自行产生事件。

  2. 更准确地说,Windows先把所有的硬件事件存入系统消息队列(system message queue),然后再放入应用程序消息队列(application message queue)。

  3. 比如可以这样从msg中得到窗口过程: (WNDPROC)GetWindowLong(msg.hwnd, GWL_WNDPROC)。

  4. 后面的论述同样适用于其他形式的软件分层结构。

  5. 指事件处理器在处理过程中又产生新的事件,从而再次触发事件处理器。

  6. 比如套接字(socket)中的accept函数。

  7. 真正的P2P网络是不需要中心服务器的,此处P2P指聊天双方是不分主客的对等关系。

  8. 有人将发行-订阅模式视为事件驱动设计的同义词,这是有道理的:在实际生活中,处于出版商与订阅者之间的邮局可作为事件管理器。

  9. MVC也可作为一种设计模式,同样是观察者模式的应用。

。总结

  • 事件是程序中令人关注的信息状态上变化。在基于事件驱动的系统中,事件包括内建事件与用户自定义事件,其中内建事件又分为底层事件和语义事件。此外,事件还有自然事件与合成事件之分。

  • Callback指能作为参数传递的函数或代码,它允许低层模块调用高层模块,使调用者与被调者从代码上解耦。异步callback在传入后并不立即被调用,使调用者与被调者从时间上解耦。

  • 控制反转一般通过callback来实现,其目的是降低模块之间的依赖性,从而降低模块的耦合度和复杂度。

  • 在框架设计中,控制反转增强了软件的可重用性、柔韧性和可扩展性,减少了用户的负担,简化了用户的代码。

  • 控制反转、依赖反转原则和依赖注射是近义词,它们的主题是控制与依赖,目的是解耦,方法是反转,而实现这一切的关键是抽象接口(包括函数指针、抽象类、接口、C++中的泛型函子和C#中的委托)。

  • 事件驱动式编程的三个步骤:实现事件处理器;注册事件处理器;实现事件循环。

  • 异步过程在主程序中以非堵塞的机制运行,即主程序不必等待该过程的返回就能继续下一步。异步机制能减少随机因素造成的资源浪费,提高系统的性能和可伸缩性。

  • 独立是异步的前提,耗时是异步的理由。

  • 事件驱动式最重要的两个特征是被动性和异步性。被动性来自控制反转,异步性来自会话切换。

  • 观察者模式又名发行/订阅模式,既是事件驱动式的简化,也是事件驱动式的核心思想。MVC架构是观察者模式在架构设计上的一个应用。

“”参考

  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

课后思考
  • 03-01 了解C++中的STL、Java中的 Collections Framework和C#中的Collection Classes。
  • 03-02 了解C++、Java和C#中的泛型机制,比较它们之间的异同以及各自在集合(collection)中的应用。
  • 03-03 当你成功构想地并实现了一个算法,是否考虑过利用泛型编程来扩大其适用范围以提高其重用性?
  • 03-04 当你发觉几个模块中有类似的算法,是否考虑过利用泛型思想进行重构?
  • 03-05 当你发觉程序中有大量类似的代码,是否考虑过用产生式编程来自动生成它们?
  • 03-06 试着利用编译器生成器(如ANTLR)自定义一种DSL,并用它来解决问题。
  • 03-07 你采用过AOP吗?它有哪些优缺点?
  • 03-08 如何合理地抽象出系统的横切关注点?
  • 03-09 请对比流程驱动式编程与事件驱动式编程之间的差异,它们各自适合哪些应用?
  • 03-10 你编写的代码是否有足够的灵活性和可扩展性?能否利用控制反转原理?
  • 03-11 你在程序中是如何处理堵塞呼叫的?是否考虑过引入异步机制?
Be Sociable, Share!

相关文章

分享/保存
 请您评分1星(很差)2星(不行)3星(一般)4星(不错)5星(很棒) (已有1人评分,平均分为:5.00 / 5)

2 comments to 冒号课堂§3.4:事件驱动

  • 事件驱动是底层平台调用你的响应事件函数的机制(狭义上),比如一个鼠标点击事件,系统是如何捕获这个事件的呢?是不是还是需要操作系统对消息队列进行轮询方法,再进行事件分类处理,对不同的消息回调上层应用程序中的响应函数呢?

    这样做能不能理解为底层平台代理你省略了轮询的过程,但是轮询,即主动去“寻找”消息还是不可或缺的,只不过是从上层应用异步地被底层所实现了?

    • hui

      首先,底层是否用轮询是其内部实现问题,无论如何上层应用采用事件驱动都是有意义的。这是两个不同层次的抽象,彼此尽可能地独立。其次,即使底层最终采用了部分轮询,那也只会是最必要、最少量的轮询,浪费CPU的OS当然不会是合格的OS。对于鼠标、键盘、网络接口等之类底层事件的捕获,也不是只采取简单的poll(轮询),根据不同硬件的特性许多也通过interrupt(中断)——即本质上仍以事件驱动为主。最后不要忘了,除了底层事件以外,文中还提到了语义事件,对于后者底层是完全不需要轮询的,因为这类事件是水到渠成的结果。打个比方,你在银行汇款只需拿号后听叫号,不用一遍遍问出纳员,这是事件驱动。作为“底层”的出纳员,每处理完一位客户自然会叫下一位,也丝毫用不着轮询。

Leave a Reply

You can use these HTML tags

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

  

  

  

This blog is kept spam free by WP-SpamFree.