回调设计模式

原创 2004年09月17日 23:42:00

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 
);

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

typedef void (CALLBACK TIMECALLBACK)(UINT uTimerID, UINT uMsg, DWORD dwUser, DWORD dw1, DWORD dw2);
typedef TIMECALLBACK FAR *LPTIMECALLBACK;

因此,用户定义的回调函数必须具有上面指定的函数原型,下面是回调函数的具体使用方法:

#include "stdio.h"
#include "windows.h"

#include "mmsystem.h" // 多媒体定时器需要包含此文件
#pragma comment(lib, "winmm.lib"// 多媒体定时器需要导入此库

void CALLBACK timer_proc(UINT uTimerID, UINT uMsg, DWORD dwUser, DWORD dw1, DWORD dw2) // 定义回调函数
{
    printf("time out./n");
}

int main()
{
    UINT nId = timeSetEvent( 1000, 0, timer_proc, 0, TIME_CALLBACK_FUNCTION|TIME_PERIODIC); // 注册回调函数
    getchar();
    timeKillEvent( nId );
    return 0;
}

运行程序,我们会看到,屏幕上每隔一秒将会打印出“time out.”信息。同时我们也应该注意到,这里的timeSetEvent是异步执行的,timeSetEvent很快执行完毕,主线程继续执行,操作系统在后台负责检查timer是否超时。

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

/// sdk.h (SDK头文件)
#ifndef __SDK_H__
#define __SDK_H__

typedef void (*HELP_CALLBACK)(const char*); // 回调函数指针
void help_me( const char* question, HELP_CALLBACK callback ); // 接口声明

#endif//__SDK_H__

/// sdk.cpp (SDK源文件,为方便,没有使用.c文件)
#include "sdk.h"
#include "stdio.h"
#include "windows.h"

HELP_CALLBACK g_callback;

void do_it() // 处理函数
{
    printf("thinking.../n");
    Sleep( 3000 );
    printf("think out./n");
    printf("call him./n");
    g_callback( "2." );
}

void help_me( const char* question, HELP_CALLBACK callback ) // 接口实现
{
    g_callback = callback// 保存回调函数指针
    printf("help_me: %s/n", question);
    do_it(); // 如果采用异步方式的话,这里一般采用创建线程的方式
}

/// app.cpp (应用程序源文件)
#include "sdk.h"
#include "stdio.h"

void got_answer( const char* msg ) // 定义回调函数
{
    printf("got_answer: %s/n", msg);
}

int main()
{
    help_me( "1+1=?", got_answer ); // 使用SDK,注册回调函数
    return 0;
}

4. C++中回调函数的设计

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

/// app.cpp ( C++风格 )
#include "sdk.h"
#include "stdio.h"

class App
{
public:
    void ask( const char* question )
    {
        help_me( question, got_answer );
    }
    static void got_answer( const char* msg )
    {
        printf("got_answer: %s/n", msg);
    }
};

int main()
{
    App app;
    app.ask( "1+1=?");
    return 0;
}

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

/// sdk.h (SDK头文件)
#ifndef __SDK_H__
#define __SDK_H__

typedef void (*HELP_CALLBACK)(const char*, unsigned long); // 回调函数指针
void help_me( const char* question, HELP_CALLBACK callbackunsigned long user_value ); // 接口声明

#endif//__SDK_H__

/// sdk.cpp (SDK源文件,为方便,没有使用.c文件)
#include "sdk.h"
#include "stdio.h"
#include "windows.h"

HELP_CALLBACK g_callback;
unsigned long g_user_value;

void do_it()
{
    printf("thinking.../n");
    Sleep( 3000 );
    printf("think out./n");
    printf("call him./n");
    g_callback( "2.", g_user_value ); // 将用户设定的数据传入
}

void help_me( const char* question, HELP_CALLBACK callbackunsigned long user_value )
{
    g_callback = callback;
    g_user_value = user_value; // 保存用户设定的数据
    printf("help_me: %s/n", question);
    do_it();
}

/// app.cpp (应用程序源文件)
#include "sdk.h"
#include "stdio.h"
#include "assert.h"

class App
{
public:
    App( const char* name ) : m_name(name)
    {
    }
    void ask( const char* question )
    {
        help_me( question, got_answer, (unsigned long)this ); // 将this指针传入
    }
    static void got_answer( const char* msg, unsigned long user_value )
    {
        App* pthis = (App*)user_value; // 转换成this指针,以访问非静态数据成员
        assert( pthis );
        printf("%s got_answer: %s/n", pthis->m_name, msg);
    }
protected:
    const char* m_name;
};

int main()
{
    App app("ABC");
    app.ask( "1+1=?");
    return 0;
}

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

5. C++中回调类编程模式

时代在不断进步,SDK不再是古老的API接口,C++面向对象编程被广泛的用到各种库中,因此回调机制也可以采用C++的一些特性来实现。

通过前面的讲解,其实我们不难发现回调的本质便是:SDK定义出一套接口规范,应用程序按照规定实现它。这样一说是不是很简单,想想我们C++中的继承,想想我们亲爱的抽象基类......于是,我们得到以下的代码:

/// sdk.h
#ifndef __SDK_H__
#define __SDK_H__

class Notifier // 回调类,应用程序需从此派生
{
public:
    virtual ~Notifier() { }
    virtual void got_answer(const char* answer) = 0; // 纯虚函数,用户必须实现它
};

class Sdk // Sdk提供服务的类
{
public:
    Sdk(Notifier* pnotifier); // 用户必须注册指向回调类的指针
    void help_me(const char* question);
protected:
    void do_it(); 
protected:
    Notifier* m_pnotifier; // 用于保存回调类的指针
};

#define//__SDK_H__

/// sdk.cpp
#include "sdk.h"
#include "windows.h"
#include <iostream>
using namespace std;

Sdk::Sdk(Notifier* pnotifier) : m_pnotifier(pnotifier)
{
}

void Sdk::help_me(const char* question)
{
    cout << "help_me: " << question << endl;
    do_it(); 
}

void Sdk::do_it()
{
    cout << "thinking..." << endl;
    Sleep( 3000 );
    cout << "think out." << endl;
    cout << "call him." << endl;
    m_pnotifier->got_answer( "2." );
}

/// app.cpp
#include "sdk.h"

class App : public Notifier // 应用程序实现一个从回调类派生的类
{
public:
    App( const char* name ) : m_sdk(this), m_name(name) // 将this指针传入
    {
    }
    void ask( const char* question )
    {
        m_sdk.help_me( question );
    }
    void got_answer( const char* answer ) // 实现纯虚接口
    {
        cout << m_name << " got_answer: " << answer << endl;
    }
protected:
    Sdk m_sdk;
    const char* m_name;
};

int main()
{
    App app("ABC");
    app.ask( "1+1=?");
    return 0;
}

哈哈,是不是很爽?Notifier将用户必须实现的回调函数都以纯虚函数的方式定义,这样用户就不得不实现它,当然如果不是非实现不可,那么我们也可以将其定义成一般的虚函数,并像析构函数那样提供一个“空”的实现,这样用户可以在关心它时才去实现它。由于这个类的作用是实现回调,所以我们不妨称之为回调类。

上面这种方式一简化,可以将Sdk与Notifier合并在一起,@#%&@#...,Stop,这样的代码每本C++书上都有,还用的着说吗(不要砸偶,鸡蛋可以,西红柿不要)。

6. C++类模板方式

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

/// sdk.h
#ifndef __SDK_H__
#define __SDK_H__

#include <iostream>
using namespace std;

template<typename Notifier>
class Sdk
{
public:
    Sdk(Notifier* pnotifier);
    void help_me(const char* question);
protected:
    void do_it();
protected:
    Notifier* m_pnotifier;
};

template<typename Notifier>
Sdk<Notifier>::Sdk(Notifier* pnotifier) : m_pnotifier(pnotifier)
{
}

template<typename Notifier>
void Sdk<Notifier>::help_me(const char* question)
{
    cout << "help_me: " << question << endl;
    do_it(); 
}

template<typename Notifier>
void Sdk<Notifier>::do_it()
{
    cout << "thinking..." << endl;
    Sleep( 3000 );
    cout << "think out." << endl;
    cout << "call him." << endl;
    m_pnotifier->got_answer( "2." );
}


#endif//__SDK_H__

/// app.cpp
#include "sdk.h"

class App
{
public:
    App( const char* name ) : m_sdk(this), m_name(name)
    {
    }
    void ask( const char* question )
    {
        m_sdk.help_me( question );
    }
    void got_answer( const char* answer )
    {
        cout << m_name << " got_answer: " << answer << endl;
    }
protected:
    Sdk<App> m_sdk;
    const char* m_name;
};

int main()
{
    App app("ABC");
    app.ask( "1+1=?");
    return 0;
}

由于现在很少有编译器支持模板的分离编译,所以迫使我们不得不把SDK的实现也放在头文件中,这对于想要隐藏SDK具体实现的朋友来说可不是一个好的选择。因此这种方式在SDK中还是用得较少。

7. 尾声

知识是灵活运用的,根据具体的需求,可以设计出各种精彩的模式,并不一一雷同。

关于回调的总结就暂告一段落,等以后想到新的方式再作补充。

(freefalcon于2004.09.17)

接口回调本质就这么简单

一、为什么会有接口回调?什么是接口回调? 二、使用实例 三、总结
  • qq379454816
  • qq379454816
  • 2016年05月31日 16:10
  • 2334

通俗易懂的让你彻底明白接口回调机制

接口回调简单理解就是:类A中调用B类的中方法C,而B又反过去调用A中的方法D,然后D称为A的回调方法。是不是心中有一万只草泥马在奔腾,到底说的是什么。感觉像是你中有我,我中有你,纠缠不清。 举个通俗易...
  • flowsky37
  • flowsky37
  • 2015年03月07日 21:00
  • 5450

最简单的方式理解“接口回调”的设计

以前对接口回调总是那么的模糊,现在仔细的看看,也没有那么的复杂。  总结如下:        接口回调需要3个类文件实现:接口类,功能类,实现类 一、接口类        1、...
  • ws595372215
  • ws595372215
  • 2016年03月31日 10:03
  • 1092

设计模式之回调模式

回调在Spring中被大量使用,那什么是回调呢?        简单的说,在A类中调用B类的C方法,然后B类调用A类中的D方法。方法D被称为回调方法。回调是实现异步的基础。经典的回调方式如下: Cl...
  • c275046758
  • c275046758
  • 2016年05月26日 17:37
  • 2935

一个经典例子让你彻彻底底理解java回调机制

以前不理解什么叫回调,天天听人家说加一个回调方法啥的,心里想我草,什么叫回调方法啊?然后自己就在网上找啊找啊找,找了很多也不是很明白,现在知道了,所谓回调:就是A类中调用B类中的某个方法C,然后B类中...
  • xiaanming
  • xiaanming
  • 2013年03月21日 23:43
  • 265142

回调设计模式

0. 引言使用过SDK的朋友应该知道“回调函数”(callback function)这个概念,但本文并不是介绍如何使用回调函数,而是站在SDK开发者的角度,讲述如何实现回调机制。1. 何为回调(ca...
  • favory
  • favory
  • 2008年06月24日 15:28
  • 3381

java设计模式-模板方法(方法回调,钩子函数)

什么叫模版方法模式: 模版方法其实就是父类定义了一整套的骨架流程,而其中的某些方法,需要子类来完成,这样子的好处是,方法流程已经定义好,子类要做的事情就是按着流程和模版自己写实现。开发中的模版方法模...
  • yingxiake
  • yingxiake
  • 2016年05月28日 18:58
  • 3665

C++ 回调函数 --函数的接口 讲解

如果参数是一个函数指针,调用者可以传递一个函数的地址给实现者,让实现者去调用它,这称为回调函数(Callback Function)。例如qsort(3)和bsearch(3)。 表 24.7. 回调...
  • xgx198831
  • xgx198831
  • 2011年08月05日 22:15
  • 6096

回调函数的面向对象设计

面向对象的回调函数过程语言c的设计方法,在面向对象的语言c++中都有对应的实现方法。而回调函数的面向对象的设计方法一直困扰着我,没有找到一种合适的面向对象的c++的方法来替代c的方法。今天在学习设计模...
  • newyher
  • newyher
  • 2017年01月04日 16:11
  • 767

设计模式:回调模式

设计模式:回调模式         回调函数,第一次见是在Java编程思想中,以及后来的观察者模式也应用了回调函数的思想。但是一直都没有重视,终于在一次面试的时候吃亏了,越来越理解为什么很多人说Ja...
  • guyuealian
  • guyuealian
  • 2016年09月02日 15:26
  • 1464
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:回调设计模式
举报原因:
原因补充:

(最多只允许输入30个字)