回调设计模式

来源:互联网 发布:云母组阵面网络事件 编辑:程序博客网 时间:2024/05/04 03:54

0. 引言使用过SDK的朋友应该知道“回调函数”(callback function)这个概念,但本文并不是介绍如何使用回调函数,而是站在SDK开发者的角度,讲述如何实现回调机制。

1. 何为回调(callback)  所谓回调,就是客户程序C调用服务程序S中的某个函数A,然后S又在某个时候反过来调用C中的某个函数B,对于C来说,这个B便叫做回调函数。例如Win32下的窗口过程函数就是一个典型的回调函数。  一般说来,C不会自己调用B,C提供B的目的就是让S来调用它,而且是C不得不提供。由于S并不知道C提供的B叫甚名谁,所以S会约定B的接口规范(函数原型),然后由C提前通过S的一个函数R告诉S自己将要使用B函数,这个过程称为回调函数的注册,R称为注册函数。  下面举个通俗的例子:  某天,我打电话向你请教问题,当然是个难题,:),你一时想不出解决方法,我又不能拿着电话在那里傻等,于是我们约定:等你想出办法后打手机通知我,这样,我就挂掉电话办其它事情去了。过了XX分钟,我的手机响了,你兴高采烈的说问题已经搞定,应该如此这般处理。故事到此结束。  这个例子说明了“异步+回调”的编程模式。其中,你后来打手机告诉我结果便是一个“回调”过程;我的手机号码必须在以前告诉你,这便是注册回调函数;我的手机号码应该有效并且手机能够接收到你的呼叫,这是回调函数必须符合接口规范。

2. 什么情况下使用回调  如果你是SDK的使用者,一旦别人制定了回调机制,那么你被迫得使用回调函数,因此这个问题只对SDK设计者有意义。  从引入的目的看,回调大致分为三种:  1) SDK有消息需要通知应用程序,比如定时器被触发;  2) SDK的执行需要应用程序的参与,比如SDK需要你提供一种排序算法;  3) SDK的操作比较费时,但又不能让应用程序阻塞在那里,于是采用异步方式,让调用函数及时返回,SDK另起线程在后台执行操作,待操作完成后再将结果通知应用程序。  经上面这样一总结,你也许会恍然大悟:原来“回调机制”无处不在啊!  是的,不光是Win32 API编程中你会用到,也不光是其它SDK编程中会用到,平时我们自己编写程序时也可能用到回调机制,这时,我们既是回调的设计者又是回调的使用者。

3. 传统SDK回调函数设计模式  Win32 SDK是这方面的典型例子,这类SDK的函数接口都是基于C语言的,SDK或者提供专门的注册函数,用于注册回调函数的地址,或者是在调用某个方法时才传入回调函数的地址,回调函数的原型也由于注册函数中的函数指针定义而受到约束。  以Win32中的多媒体定时器函数为例,其原型为: MMRESULT timeSetEvent(   UINT uDelay,  // 定时器时间间隔,以毫秒为单位                UINT uResolution,             LPTIMECALLBACK lpTimeProc,  // 回调函数地址   DWord dwUser,  // 用户设定的数据               UINT fuEvent                );

 其中第三个参数便是用于注册回调函数的,第四个参数用于设定用户自定义数据,其作用将在后文说明,

view plaincopy to clipboardprint?
  1. LPTIMECALLBACK的定义为:   
  2.   
  3. typedef void (CALLBACK TIMECALLBACK)(UINT uTimerID, UINT uMsg, DWORD dwUser, DWORD dw1, DWORD dw2);   
  4.   
  5. typedef TIMECALLBACK FAR *LPTIMECALLBACK;   
  6.   
  7. 因此,用户定义的回调函数必须具有上面指定的函数原型,下面是回调函数的具体使用方法:  
  8.  
  9. #include "stdio.h"  
  10.  
  11. #include "windows.h"  
  12.  
  13.  
  14.  
  15. #include "mmsystem.h" // 多媒体定时器需要包含此文件  
  16.  
  17. #pragma comment(lib, "winmm.lib") // 多媒体定时器需要导入此库   
  18.   
  19.   
  20.   
  21. void CALLBACK timer_proc(UINT uTimerID, UINT uMsg, DWORD dwUser, DWORD dw1, DWORD dw2) // 定义回调函数   
  22.   
  23. {   
  24.   
  25.  printf("time out./n");   
  26.   
  27. }   
  28.   
  29.   
  30.   
  31. int main()   
  32.   
  33. {   
  34.   
  35.  UINT nId = timeSetEvent( 1000, 0, timer_proc, 0, TIME_CALLBACK_FUNCTIONTIME_PERIODIC); // 注册回调函数   
  36.   
  37.  getchar();   
  38.   
  39.  timeKillEvent( nId );   
  40.   
  41.  return 0;   
  42.   
  43. }  
view plaincopy to clipboardprint?
  1. 运行程序,我们会看到,屏幕上每隔一秒将会打印出“time out.”信息。同时我们也应该注意到,这里的timeSetEvent是异步执行的,timeSetEvent很快执行完毕,主线程继续执行,<A title=操作系统 href="http://www.viphot.com/caozuoxitong/" target=_blank>操作系统</A>在后台负责检查timer是否超时。  

 前面已经说过,本文的是站在SDK设计者的角度来看待问题,这里,我们就把前面那个通俗的例子变成程序,如下:

view plaincopy to clipboardprint?
  1. /// sdk.h (SDK头文件)  
  2.  
  3. #ifndef __SDK_H__  
  4.  
  5. #define __SDK_H__   
  6.   
  7.   
  8.   
  9. typedef void (*HELP_CALLBACK)(const char*); // 回调函数指针   
  10.   
  11. void help_me( const char* question, HELP_CALLBACK callback ); // 接口声明  
  12.  
  13.  
  14.  
  15. #endif//__SDK_H__   
  16.   
  17.   
  18.   
  19. /// sdk.cpp (SDK源文件,为方便,没有使用.c文件)  
  20.  
  21. #include "sdk.h"  
  22.  
  23. #include "stdio.h"  
  24.  
  25. #include "windows.h"   
  26.   
  27.   
  28.   
  29. HELP_CALLBACK g_callback;   
  30.   
  31.   
  32.   
  33. void do_it() // 处理函数   
  34.   
  35. {   
  36.   
  37.  printf("thinking.../n");   
  38.   
  39.  Sleep( 3000 );   
  40.   
  41.  printf("think out./n");   
  42.   
  43.  printf("call him./n");   
  44.   
  45.  g_callback( "2." );   
  46.   
  47. }   
  48.   
  49.   
  50.   
  51. void help_me( const char* question, HELP_CALLBACK callback ) // 接口实现   
  52.   
  53. {   
  54.   
  55.  g_callback = callback; // 保存回调函数指针   
  56.   
  57.  printf("help_me: %s/n", question);   
  58.   
  59.  do_it(); // 如果采用异步方式的话,这里一般采用创建线程的方式   
  60.   
  61. }   
  62.   
  63.   
  64.   
  65. /// app.cpp (应用程序源文件)  
  66.  
  67. #include "sdk.h"  
  68.  
  69. #include "stdio.h"   
  70.   
  71.   
  72.   
  73. void got_answer( const char* msg ) // 定义回调函数   
  74.   
  75. {   
  76.   
  77.  printf("got_answer: %s/n", msg);   
  78.   
  79. }   
  80.   
  81.   
  82.   
  83. int main()   
  84.   
  85. {   
  86.   
  87.  help_me( "1+1=?", got_answer ); // 使用SDK,注册回调函数   
  88.   
  89.  return 0;   
  90.   
  91. }  

4. C++中回调函数的设计  C++的类中也可以使用类似上面的设计方式。  如果SDK采用C语言接口,应用程序使用C++编程方式,那么类成员函数由于具有隐含的this指针而不能赋值给普通函数指针,解决方法很简单,就是为其加上static关键字。  以上面的程序为例,这里我们只看应用程序的代码:

view plaincopy to clipboardprint?
  1. /// app.cpp ( C++风格 )  
  2.  
  3. #include "sdk.h"  
  4.  
  5. #include "stdio.h"   
  6.   
  7.   
  8.   
  9. class App   
  10.   
  11. {   
  12.   
  13. public:   
  14.   
  15.  void ask( const char* question )   
  16.   
  17.  {   
  18.   
  19.   help_me( question, got_answer );   
  20.   
  21.  }   
  22.   
  23.  static void got_answer( const char* msg )   
  24.   
  25.  {   
  26.   
  27.   printf("got_answer: %s/n", msg);   
  28.   
  29.  }   
  30.   
  31. };   
  32.   
  33.   
  34.   
  35. int main()   
  36.   
  37. {   
  38.   
  39.  App app;   
  40.   
  41.  app.ask( "1+1=?");   
  42.   
  43.  return 0;   
  44.   
  45. }  

上面这种方式有个明显的缺点:由于got_answer是静态成员函数,所以它不能访问类的非静态成员变量,这可不是一件好事情。为了解决此问题,作为回调函数的设计者,你有必要为其增添一个参数,用于传递用户所需的值,如下:

view plaincopy to clipboardprint?
  1. /// sdk.h (SDK头文件)  
  2.  
  3. #ifndef __SDK_H__  
  4.  
  5. #define __SDK_H__   
  6.   
  7.   
  8.   
  9. typedef void (*HELP_CALLBACK)(const char*, unsigned long); // 回调函数指针   
  10.   
  11. void help_me( const char* question, HELP_CALLBACK callback, unsigned long user_value ); // 接口声明  
  12.  
  13.  
  14.  
  15. #endif//__SDK_H__   
  16.   
  17.   
  18.   
  19. /// sdk.cpp (SDK源文件,为方便,没有使用.c文件)  
  20.  
  21. #include "sdk.h"  
  22.  
  23. #include "stdio.h"  
  24.  
  25. #include "windows.h"   
  26.   
  27.   
  28.   
  29. HELP_CALLBACK g_callback;   
  30.   
  31. unsigned long g_user_value;   
  32.   
  33.   
  34.   
  35. void do_it()   
  36.   
  37. {   
  38.   
  39.  printf("thinking.../n");   
  40.   
  41.  Sleep( 3000 );   
  42.   
  43.  printf("think out./n");   
  44.   
  45.  printf("call him./n");   
  46.   
  47.  g_callback( "2.", g_user_value ); // 将用户设定的数据传入   
  48.   
  49. }   
  50.   
  51.   
  52.   
  53. void help_me( const char* question, HELP_CALLBACK callback, unsigned long user_value )   
  54.   
  55. {   
  56.   
  57.  g_callback = callback;   
  58.   
  59.  g_user_value = user_value; // 保存用户设定的数据   
  60.   
  61.  printf("help_me: %s/n", question);   
  62.   
  63.  do_it();   
  64.   
  65. }   
  66.   
  67.   
  68.   
  69. /// app.cpp (应用程序源文件)  
  70.  
  71. #include "sdk.h"  
  72.  
  73. #include "stdio.h"  
  74.  
  75. #include "assert.h"   
  76.   
  77.   
  78.   
  79. class App   
  80.   
  81. {   
  82.   
  83. public:   
  84.   
  85.  App( const char* name ) : m_name(name)   
  86.   
  87.  {   
  88.   
  89.  }   
  90.   
  91.  void ask( const char* question )   
  92.   
  93.  {   
  94.   
  95.   help_me( question, got_answer, (unsigned long)this ); // 将this指针传入  
view plaincopy to clipboardprint?
  1. <PRE class=csharp name="code">}   
  2.   
  3.  static void got_answer( const char* msg, unsigned long user_value )   
  4.   
  5.  {   
  6.   
  7.   App* pthis = (App*)user_value; // 转换成this指针,以访问非静态数据成员   
  8.   
  9.   assert( pthis );   
  10.   
  11.   printf("%s got_answer: %s/n", pthis->m_name, msg);   
  12.   
  13.  }   
  14.   
  15. protected:   
  16.   
  17.  const char* m_name;   
  18.   
  19. };   
  20.   
  21.   
  22.   
  23. int main()   
  24.   
  25. {   
  26.   
  27.  App app("ABC");   
  28.   
  29.  app.ask( "1+1=?");   
  30.   
  31.  return 0;   
  32.   
  33. }   
  34.   
  35.   
  36.   
  37. </PRE>  

这里的user_value被设计成unsigned long,它既可以传递整型数据,也可以传递一个地址值(因为它们在32位机器上宽度都为32),有了地址,那么像结构体变量、类对象等都可以访问了。

5. C++中回调类编程模式  时代在不断进步,SDK不再是古老的API接口,C++面向对象编程被广泛的用到各种库中,因此回调机制也可以采用C++的一些特性来实现。  通过前面的讲解,其实我们不难发现回调的本质便是:SDK定义出一套接口规范,应用程序按照规定实现它。这样一说是不是很简单,

想想我们C++中的继承,想想我们亲爱的抽象基类......于是,我们得到以下的代码:

view plaincopy to clipboardprint?
  1. /// sdk.h  
  2.  
  3. #ifndef __SDK_H__  
  4.  
  5. #define __SDK_H__   
  6.   
  7.   
  8.   
  9. class Notifier // 回调类,应用程序需从此派生   
  10.   
  11. {   
  12.   
  13. public:   
  14.   
  15.  virtual ~Notifier() { }   
  16.   
  17.  virtual void got_answer(const char* answer) = 0; // 纯虚函数,用户必须实现它   
  18.   
  19. };   
  20.   
  21.   
  22.   
  23. class Sdk // Sdk提供服务的类   
  24.   
  25. {   
  26.   
  27. public:   
  28.   
  29.  Sdk(Notifier* pnotifier); // 用户必须注册指向回调类的指针   
  30.   
  31.  void help_me(const char* question);   
  32.   
  33. protected:   
  34.   
  35.  void do_it();     
  36.   
  37. protected:   
  38.   
  39.  Notifier* m_pnotifier; // 用于保存回调类的指针   
  40.   
  41. };  
  42.  
  43.  
  44.  
  45. #define//__SDK_H__   
  46.   
  47.   
  48.   
  49. /// sdk.cpp  
  50.  
  51. #include "sdk.h"  
  52.  
  53. #include "windows.h"  
  54.  
  55. #include <iostream>   
  56.   
  57. using namespace std;   
  58.   
  59.   
  60.   
  61. Sdk::Sdk(Notifier* pnotifier) : m_pnotifier(pnotifier)   
  62.   
  63. {   
  64.   
  65. }   
  66.   
  67.   
  68.   
  69. void Sdk::help_me(const char* question)   
  70.   
  71. {   
  72.   
  73.  cout << "help_me: " << question << endl;   
  74.   
  75.  do_it();    
  76.   
  77. }   
  78.   
  79.   
  80.   
  81. void Sdk::do_it()   
  82.   
  83. {   
  84.   
  85.  cout << "thinking..." << endl;   
  86.   
  87.  Sleep( 3000 );   
  88.   
  89.  cout << "think out." << endl;   
  90.   
  91.  cout << "call him." << endl;   
  92.   
  93.  m_pnotifier->got_answer( "2." );   
  94.   
  95. }   
  96.   
  97.   
  98.   
  99. /// app.cpp  
  100.  
  101. #include "sdk.h"   
  102.   
  103.   
  104.   
  105. class App : public Notifier // 应用程序实现一个从回调类派生的类   
  106.   
  107. {   
  108.   
  109. public:   
  110.   
  111.  App( const char* name ) : m_sdk(this), m_name(name) // 将this指针传入   
  112.   
  113.  {   
  114.   
  115.  }   
  116.   
  117.  void ask( const char* question )   
  118.   
  119.  {   
  120.   
  121.   m_sdk.help_me( question );   
  122.   
  123.  }   
  124.   
  125.  void got_answer( const char* answer ) // 实现纯虚接口   
  126.   
  127.  {   
  128.   
  129.   cout << m_name << " got_answer: " << answer << endl;   
  130.   
  131.  }   
  132.   
  133. protected:   
  134.   
  135.  Sdk m_sdk;   
  136.   
  137.  const char* m_name;   
  138.   
  139. };   
  140.   
  141.   
  142.   
  143. int main()   
  144.   
  145. {   
  146.   
  147.  App app("ABC");   
  148.   
  149.  app.ask( "1+1=?");   
  150.   
  151.  return 0;   
  152.   
  153. }  

哈哈,是不是很爽?Notifier将用户必须实现的回调函数都以纯虚函数的方式定义,这样用户就不得不实现它,当然如果不是非实现不可,那么我们也可以将其定义成一般的虚函数,并像析构函数那样提供一个“空”的实现,这样用户可以在关心它时才去实现它。由于这个类的作用是实现回调,所以我们不妨称之为回调类。  上面这种方式一简化,可以将Sdk与Notifier合并在一起,@#%&@#...,Stop,这样的代码每本C++书上都有,还用的着说吗(不要砸偶,鸡蛋可以,西红柿不要)。

6. C++类模板方式  上面的方式有继承还有虚函数,某些朋友可能不喜欢其中带来的额外代价,那么怎么才能消除这二者呢?本节的标题已给出了答案——类模板,代码为:

view plaincopy to clipboardprint?
  1. /// sdk.h  
  2.  
  3. #ifndef __SDK_H__  
  4.  
  5. #define __SDK_H__  
  6.  
  7.  
  8.  
  9. #include <iostream>   
  10.   
  11. using namespace std;   
  12.   
  13.   
  14.   
  15. template<typename Notifier>   
  16. <PRE class=csharp name="code">    
  17.   
  18. class Sdk   
  19.   
  20. {   
  21.   
  22. public:   
  23.   
  24.  Sdk(Notifier* pnotifier);   
  25.   
  26.  void help_me(const char* question);   
  27.   
  28. protected:   
  29.   
  30.  void do_it();   
  31.   
  32. protected:   
  33.   
  34.  Notifier* m_pnotifier;   
  35.   
  36. };   
  37.   
  38.   
  39.   
  40. template<typename Notifier>   
  41.   
  42. Sdk<Notifier>::Sdk(Notifier* pnotifier) : m_pnotifier(pnotifier)   
  43.   
  44. {   
  45.   
  46. }   
  47.   
  48.   
  49.   
  50. template<typename Notifier>   
  51.   
  52. void Sdk<Notifier>::help_me(const char* question)   
  53.   
  54. {   
  55.   
  56.  cout << "help_me: " << question << endl;   
  57.   
  58.  do_it();    
  59.   
  60. }   
  61.   
  62.   
  63.   
  64. template<typename Notifier>   
  65.   
  66. void Sdk<Notifier>::do_it()   
  67.   
  68. {   
  69.   
  70.  cout << "thinking..." << endl;   
  71.   
  72.  Sleep( 3000 );   
  73.   
  74.  cout << "think out." << endl;   
  75.   
  76.  cout << "call him." << endl;   
  77.   
  78.  m_pnotifier->got_answer( "2." );   
  79.   
  80. }  
  81.  
  82.  
  83.  
  84.  
  85.  
  86. #endif//__SDK_H__   
  87.   
  88.   
  89.   
  90. /// app.cpp  
  91.  
  92. #include "sdk.h"   
  93.   
  94.   
  95.   
  96. class App   
  97.   
  98. {   
  99.   
  100. public:   
  101.   
  102.  App( const char* name ) : m_sdk(this), m_name(name)   
  103.   
  104.  {   
  105.   
  106.  }   
  107.   
  108.  void ask( const char* question )   
  109.   
  110.  {   
  111.   
  112.   m_sdk.help_me( question );   
  113.   
  114.  }   
  115.   
  116.  void got_answer( const char* answer )   
  117.   
  118.  {   
  119.   
  120.   cout << m_name << " got_answer: " << answer << endl;   
  121.   
  122.  }   
  123.   
  124. protected:   
  125.   
  126.  Sdk<App> m_sdk;   
  127.   
  128.  const char* m_name;   
  129.   
  130. };   
  131.   
  132.   
  133.   
  134. int main()   
  135.   
  136. {   
  137.   
  138.  App app("ABC");   
  139.   
  140.  app.ask( "1+1=?");   
  141.   
  142.  return 0;   
  143.   
  144. }   
  145. 由于现在很少有编译器支持模板的分离编译,所以迫使我们不得不把SDK的实现也放在头文件中,这对于想要隐藏SDK具体实现的朋友来说可不是一个好的选择。因此这种方式在SDK中还是用得较少。   </PRE>  

7. 尾声  知识是灵活运用的,根据具体的需求,可以设计出各种精彩的模式,并不一一雷同。  关于回调的总结就暂告一段落,等以后想到新的方式再作补充。

原创粉丝点击