前面我们谈到了功能扩展对维护一个软件的巨大作用。实际上,正是因为功能在不断地扩展,才使得我们的很多软件质量在下降。因此,如何进行功能扩展,我们不得不察。每当新功能到来的时候,不用急急匆匆就开始编码,我们应当仔细思考我们的设计,即使是时间非常紧张的项目。用更多的时间去思考与设计,才会用更少的时间去做更简单的设计与编码。在这里,我提倡的是设计应当简单到发指,因为它体现的是一种精巧绝伦,它会使我们的思路更清晰,维护更简单,变更更容易。只有经过仔细的思考,才会做出精巧绝伦的设计,那是我们的目标。在这方面,“小步快跑”与“两顶帽子”的方法可以大大降低我们的设计难度,因为我们不是神,而是人。
上一章,我们用“抽取接口”的方法,替代万恶的if语句,从而实现了功能扩展。注意,不是所有的if语句都需要这样调整,只有那些真正需要扩展的时候才应该这样调整。如果if语句只有2、3个条件分支,并且不太可能扩展,我们真的不必这样调整,或者当这样的功能扩展到来的时候再进行调整。记住,过度设计与恰到好处的设计只有一线之隔。
除此之外,我们还有很多的办法来扩展我们的功能,其中一种扩展叫过程的扩展,我们可以这样设计:
前面代码复用的部分我们提到,解决处理过程中相同或相似的代码最好的办法就是使用模板模式。首先将那些相同的代码抽取出来形成函数,将这些函数抽象并升级为抽象类及接口,然后将各自不同的代码统一函数名,放在各个实现类中各自去实现。这样,代码复用的问题就解决了。
但是,毫无疑问我们都不是先知,永远都无法预测未来系统会变成什么样儿。比较常见的需求变更之一就是处理步骤的变更。现在我们的处理有1、2、3步,而今后可能还有5、6、7步,甚至某项步骤可能会插入到现有步骤的中间。当日后这样的变更发生的时候,我们又希望符合OCP原则而不改动现有代码时,又应当怎样设计呢?嗯,是个问题。
我过去就曾无数次遇到过这样的问题,其中一个令我印象深刻的就是一次平台控件的设计。在一次平台开发中,我设计了许多的控件,如文本框、下拉框、单选框、复选框……开发这些控件的目的是使其它开发者在设计报表的过滤条件时不用再写任何代码,选择控件就可以了。起初,我为所有控件都提供了draw()和beUsed()方法,用于绘制控件和判断该条件在查询时是否被使用。随着控件品种的增加,一些控件需要在绘制前要执行一个查询,如那些多选框、下拉框等等。为此我准备设计了一个getItems()方法,只要这些控件定义了各自的查询语句,就可以通过该方法查询并返回结果。但问题是,前面已经设计好的控件不用这个方法,我不希望因为这个功能的扩展影响了前面那些控件,这该怎么设计呢?
每次面对这样的问题时,一种叫做“钩子(hook)”的设计就可以派上用场了。什么叫“钩子”?它是一个空函数,调用它就如同什么都没有调用一般。但钩子如果被放在了抽象类中,作用就非常大了。如果抽象类的子类要使用它时,则重载这个函数,为其编写各自的代码,完成相应的操作;而其它的子类如果不使用它,则什么也不用做。当系统在调用各个子类时,被重载的子类就会去调用子类中的函数,而其它没有被重载的子类则会去调用抽象类中的“钩子”,就如同什么都没有做一样。
在该示例中,getItems()就是一个“钩子”,它首先被定义在父类AbstractControl中。AbstractControl是一个抽象类,但getItems()在里面不是被定义成一个抽象方法,而是一个普通方法,因为它不需要每个子类都去实现它,不使用的子类就不用再实现它了。
那些在绘制前不需要查询的控件,如DefaultControl,在继承父类的时候不用去重载getItems(),因此系统在绘制它们时,该函数就如同不存在一般。然而那些需要查询的控件,如QueryControl,就需要重载这个函数:
这样,当系统在绘制DefaultControl的时候不会去查询数据库,而绘制QueryControl的时候则先去进行一个数据库查询。现在我们来检测一下该可扩展点的设计能否满足OCP原则的要求。现在新需求来了,要绘制这么一个组合控件:
这个组合控件由四个下拉框组成,分别代表2个年度与2个月份,因此它在执行查询时,会提交到后台这4个数据。然而,我们希望这个控件在提交给查询模块时,应当是2个数据:某年某月的1日,和某年某月的最后一天。也就是说,该控件在提交参数给查询模块的时候需要进行一个参数转换,而不是直接传递给查询模块。
先看看我们现有的设计吧:当控件将参数提交给后台以后,控件会直接将参数传递给查询模块。但为了实现这样一个新需求,我们需要所有控件在这个地方硬生生插入一个数据转换的功能。如果真的这样修改了,整个系统就因小失大了。幸运的是,我们在这个地方有可扩展设计。
整个设计修改了父类AbstractControl,增加了transform()方法,然后创建了新的控件类MonthRangeControl,不能说完全没有修改原程序,但已经在最大限度上满足了OCP原则。整个设计如图:
特别说明:希望网友们在转载本文时,应当注明作者或出处,以示对作者的尊重,谢谢!
HOOK API (一)——HOOK基础+一个鼠标钩子实例
最近在做毕业设计,有一个功能是需要实现对剪切板的监控和进程的防终止保护。原本想从内核层实现,但没有头绪。最后决定从调用层入手,即采用HOOK API的技术来挂钩相应的API,从而实现预期的功能。在这样的需求下,就开始学习了HOOK API。
HOOK(钩子,挂钩)是一种实现Windows平台下类似于中断的机制。HOOK机制允许应用程序拦截并处理Windows消息或指定事件,当指定的消息发出后,HOOK程序就可以在消息到达目标窗口之前将其捕获,从而得到对消息的控制权,进而可以对该消息进行处理或修改,加入我们所需的功能。钩子按使用范围分,可分为线程钩子和系统钩子,其中,系统钩子具有相当大的功能,几乎可以实现对所有Windows消息的拦截、处理和监控。这项技术涉及到两个重要的API,一个是SetWindowsHookEx,***钩子;另一个是UnHookWindowsHookEx,卸载钩子。
本文使用的HOOK API技术,是指截获系统或进程对某个API函数的调用,使得API的执行流程转向我们指定的代码段,从而实现我们所需的功能。Windows下的每个进程均拥有自己的地址空间,并且进程只能调用其地址空间内的函数,因此HOOK API尤为关键的一步是,设法将自己的代码段注入到目标进程中,才能进一步实现对该进程调用的API进行拦截。然而微软并没有提供HOOK API的调用接口,这就需要开发者自己编程实现,大家所熟知的防毒软件、防火墙软件等均采用HOOK API实现。
一般来说,HOOK API由两个组成部分,即实现HOOK API的DLL文件,和启动注入的主调程序。本文采用HOOK API 技术对剪切板相关的API 函数进行拦截,从而实现对剪切板内容的监控功能,同样使用该技术实现进程防终止功能。其中DLL文件支持HOOK API的实现,而主调客户端程序将在初始化时把带有HOOK API功能的DLL随着鼠标钩子的加载注入到目标进程中,这里的鼠标钩子属于系统钩子。
(1) 键盘钩子和低级键盘钩子可以监视各种键盘消息。
(2) 鼠标钩子和低级鼠标钩子可以监视各种鼠标消息。
(3) 外壳钩子可以监视各种Shell事件消息。比如启动和关闭应用程序。
(4) 日志钩子可以记录从系统消息队列中取出的各种事件消息。
(5) 窗口过程钩子监视所有从系统消息队列发往目标窗口的消息。
此外,还有一些特定事件的钩子提供给我们使用,不一一列举。
下面描述常用的Hook类型:
Hook传递指针到CWPRETSTRUCT结构,再传递到Hook子程。CWPRETSTRUCT结构包含了来自处理消息的窗口过程的返回值,同样也包括了与这个消息关联的消息参数。
在以下事件之前,系统都会调用WH_CBT Hook子程,这些事件包括:
1. 激活,建立,销毁,最小化,最大化,移动,改变尺寸等窗口事件;
3. 来自系统消息队列中的移动鼠标,键盘事件;
4. 设置输入焦点事件;
5. 同步系统消息队列事件。
Hook子程的返回值确定系统是否允许或者防止这些操作中的一个。
在系统调用系统中与其他Hook关联的Hook子程之前,系统会调用WH_DEBUG Hook子程。你可以使用这个Hook来决定是否允许系统调用与其他Hook关联的Hook子程。
当应用程序的前台线程处于空闲状态时,可以使用WH_FOREGROUNDIDLE Hook执行低优先级的任务。当应用程序的前台线程大概要变成空闲状态时,系统就会调用WH_FOREGROUNDIDLE Hook子程。
Hook是全局Hook,它不能象线程特定Hook一样使用。WH_JOURNALPLAYBACK Hook返回超时值,这个值告诉系统在处理来自回放Hook当前消息之前需要等待多长时间(毫秒)。这就使Hook可以控制实时事件的回放。WH_JOURNALPLAYBACK是system-wide local hooks,它們不會被注射到任何行程位址空間。(估计按键精灵是用这个hook做的)
hooks,它們不會被注射到任何行程位址空間。
WH_MOUSE_LL Hook监视输入到线程消息队列中的鼠标消息。
Hook监视所有应用程序消息。WH_MSGFILTER 和 WH_SYSMSGFILTER Hooks使我们可以在模式循环期间过滤消息,这等价于在主消息循环中过滤消息。通过调用CallMsgFilter function可以直接的调用WH_MSGFILTER Hook。通过使用这个函数,应用程序能够在模式循环期间使用相同的代码去过滤消息,如同在主消息循环里一样。
外壳应用程序可以使用WH_SHELL Hook去接收重要的通知。当外壳应用程序是激活的并且当顶层窗口建立或者销毁时,系统调用WH_SHELL Hook子程。
只要有个top-level、unowned 窗口被产生、起作用、或是被摧毁;
当Taskbar需要重画某个按钮;
当系统需要显示关于Taskbar的一个程序的最小化形式;
当目前的键盘布局状态改变;
按照惯例,外壳应用程序都不接收WH_SHELL消息。所以,在应用程序能够接收WH_SHELL消息之前,应用程序必须调用SystemParametersInfo function注册它自己。
以上是13种常用的hook类型!
主要有线程钩子和系统钩子:
(1) 线程钩子监视指定线程的事件消息。
(2) 系统钩子监视系统中的所有线程的事件消息。因为系统钩子会影响系统中所有的应用程序,所以钩子函数必须放在独立的动态链接库(DLL)
中。这是系统钩子和线程钩子很大的不同之处。
(1) 如果对于同一事件(如鼠标消息)既***了线程钩子又***了系统钩子,那么系统会自动先调用线程钩子,然后调用系统钩子。
(2) 对同一事件消息可***多个钩子处理过程,这些钩子处理过程形成了钩子链。当前钩子处理结束后应把钩子信息传递给下一个钩子函数。而且最近***的钩子放在链的开始,而最早***的钩子放在最后,也就是后加入的先获得控制权。
(3) 钩子特别是系统钩子会消耗消息处理时间,降低系统性能。只有在必要的时候才***钩子,在使用完毕后要及时卸载。
编写钩子程序的步骤分为三步:定义钩子函数、***钩子和卸载钩子。
钩子函数是一种特殊的回调函数。钩子监视的特定事件发生后,系统会调用钩子函数进行处理。不同事件的钩子函数的形式是各不相同的。下面以鼠标钩子函数举例说明钩子函数的原型:
参数wParam和 lParam包含所钩消息的信息,比如鼠标位置、状态,键盘按键等。nCode包含有关消息本身的信息,比如是否从消息队列中移出。
我们先在钩子函数中实现自定义的功能,然后调用函数 CallNextHookEx.把钩子信息传递给钩子链的下一个钩子函数。CallNextHookEx.的原型如下:
当然也可以通过直接返回TRUE来丢弃该消息,就阻止了该消息的传递。
在程序初始化的时候,调用函数SetWindowsHookEx***钩子。其函数原型为:
参数idHook表示钩子类型,它是和钩子函数类型一一对应的。比如,WH_KEYBOARD表示***的是键盘钩子,WH_MOUSE表示是鼠标钩子等等。
Lpfn是钩子函数的地址。
HMod是钩子函数所在的实例的句柄。对于线程钩子,该参数为NULL;对于系统钩子,该参数为钩子函数所在的DLL句柄。
dwThreadId 指定钩子所监视的线程的线程号。对于全局钩子,该参数为NULL。
当不再使用钩子时,必须及时卸载。简单地调用函数:
值得注意的是线程钩子和系统钩子的钩子函数的位置有很大的差别。线程钩子一般在当前线程或者当前线程派生的线程内,而系统钩子必须放在独立的动态链接库中,实现起来要麻烦一些。
参看上一小结可知,编写钩子程序的三个步奏是:
(1) 定义钩子函数:
还需要注意一点:系统钩子必须放在独立的动态链接库中。由此,程序分为两个部分:一个是钩子程序动态链接库,实现了鼠标钩子程序;另一个是MFC操作窗体,对DLL进行加载和卸载,即对DLL进行测试。
/* 共享代码段,所有线程共享 */
/* 定义低级鼠标子函数 */ // 有鼠标消息时,将其发给主程序
/* ***低级鼠标子函数,从而截获系统所有的鼠标信息 */
/* 卸载低级鼠标钩子 */