关键字:回调函数,回调类,设计模式
摘要:
本文通过引入回调类设计模式,简化在面向对象中的回调机制,特别适合于动态链接库程序对主程序的函数回调。本文给出一个具体而微的例子,将一个面向过程的C语言世界中非常普遍的回调机制,在面向对象的世界里进行了转化,采用回调类设计模式,现实问题得到优雅的解决。
背景介绍:
WLTP是无线话务量测试平台,是基于3GLT开发的话务量测试工具,在3GLT后台Bam上,开发了我们自己的插件:WLTP2Daemon.dll.
WLTP测试工具后台BAM,各组件的调用关系如下图所示:
Bamadmin.exe是3GLT提供的后台主程序;
WLTP2Daemon.dll是我们自己开发的后台插件;
DaemonService.dll是我们提取的后台服务基础设施模块(CBB模块);
Commdll.dll是用于Socket通讯的公共组件;
WltpPlugin.dll是3GLT后台UCC的插件,是与测试人员交互的窗口;
WLTP2Daemon.dll加载DaemonService.dll模块后会启动后台服务功能(目前只包含FTP服务功能),当单板请求FTP服务加载单板软件时,提供FTP服务的模块是DaemonService.dll,但我们需要有能力将前台单板的加载情况汇报给后台WltpPlugin.dll插件。那么DaemonService.dll也需要使用Commdll.dll.这种想法将会导致DaemonService.dll没法成为CBB模块,因为它不够独立,依赖于其他别的组件。然后,需求是要实现的,那该怎么办呢,一种想法是通过消息机制,DaemonService.dll发送加载进度情况的消息给WLTP2Daemon.dll, WLTP2Daemon.dll收到进度通知消息后,通过Commdll.dll发送加载进度消息给WltpPlugin.dll.另外一种想法就是通过注册回调函数的机制,WLTP2Daemon.dll注册一个更新进度通知的回调函数给DaemonService.dll,当DaemonService.dll提供FTP服务时调用回调函数,通过回调函数有能力将加载进度消息上报给WltpPlugin.dll.
第一种方法有松耦合的优势,但是由于我们的WLTP2Daemon.dll自身也是dll,而且属于后台服务插件,并非是有标题的可见窗口,通过消息机制不太容易实现,有一定的技术难度。那么只能采用第二种方法。然而,第二种方法虽然可以通过C语言的方式写注册回调函数来解决,但实现上毕竟不够优雅,而且也没能利用C++面向对象编程的优势。客户端程序在使用DaemonService.dll的时候,必须通过不太直观且不太易用的注册回调函数,将自身实现的回调函数指针注册进DaemonService.dll.
CBB模块的一个原则是尽可能的让用户使用起来最简单。就如同最近看到的新闻,Boost的日志模块评审不通过,其驳回理由基本上是不便于用户使用。现实开发中理想与实用是一对矛盾,CBB的接口要简单,同时还要给客户最大的定制的自由度。下文中,将介绍采用C++的注册回调类的机制来取得这种折衷的恰到好处的优雅的平衡。
实现过程:
DaemonService.dll目前只是包容了FTP服务模块,但接口设计相当简单,可扩展性较好。看一下目前DaemonService.dll的接口:
#include "stdafx.h"
#include "IProgressNotifier.h"
//提供给客户程序使用的头文件
#ifndef DaemonService_API
#define DaemonService_APIextern"C"_declspec(dllimport)
#endif
DaemonService_APIvoid_stdcallRegisterNotifier(IProgressNotifier* pnotifier);
DaemonService_APIvoid_stdcallStartFtpServer(); //启动Ftp服务
DaemonService_APIvoid_stdcallStopFtpServer(); //停止Ftp服务
//设置Ftp服务IP
DaemonService_APIvoid_stdcallSetFtpServerIP(CStringServerIP);
//设置Ftp服务的用户名,密码,服务根路径
DaemonService_APIvoid_stdcallInitFtpServer(CStringuser, CStringpassword, CStringroot);
在上面的函数中:RegisterNotifier(IProgressNotifier* pnotifier);
这个函数是本文所要描述的重点,也就是上文中提到的注册回调类函数。注意区别于注册回调函数!在实际编程中我发现采取上述机制后,不再需要使用C语言的typedef定义函数指针,注册回调函数的声明也变得异常简单,只需要注册一个抽象类的指针即可。由于抽象类对回调函数进行了包装,所以对客户端而言使用回调机制将变得更加简单。
IProgressNotifier是抽象类,其包装了更新进度通知的回调函数接口:
classIProgressNotifier
{
public:
// 被封装的回调函数,子类将覆写该函数完成更新加载进度状态的功能
// 其中dwIpAddr是请求加载的单板IP地址,wPort是其端口,byPercent是加载进度百分比
virtualvoidUpdateProgressStatus(DWORDdwIpAddr, WORDwPort, BYTEbyPercent) = 0;// 纯虚函数,用户必须实现它
IProgressNotifier(){};
virtual ~IProgressNotifier(){};
};
上述抽象类中的UpdateProgressStatus(DWORDdwIpAddr, WORDwPort, BYTEbyPercent)函数就是回调函数的抽象接口定义。只是从原先C语言的函数指针形式摇身一变,成为直观的类的内部函数的形式,使用起来自然比C的方式要简单直观一些,特别是在面向对象编程的世界里,至少在理念上是具有优势的。
客户程序(WLTP2Daemon.dll)首先通过继承IProgressNotifier,并且覆写IProgressNotifier中的虚函数,覆写的UpdateProgressStatus(DWORDdwIpAddr, WORDwPort, BYTEbyPercent)根据传入的参数,按照业务需求,自由定制上报加载进度的代码,给客户最大的灵活度。
然后,客户程序在加载DaemonService.dll的时候,首先new一个IProgressNotifier的具体实现类的对象,并且通过RegisterNotifier(IProgressNotifier* pnotifier)将该具体实现类的指针传递给DaemonService.dll,DaemonService.dll会将传入的具体实现类的指针保存下来,根据多态的原理,后续DaemonService.dll将有能力通过该指针调用客户程序覆写的进度通知的功能。DaemonService.dll中如何保存、使用客户程序传递过来的进度通知的具体类指针呢?
为了方便起见,编写了一个类PNCallbackManager用于回调类的注册,回调函数的调用等:
// PNCallbackManager.h
#include "IProgressNotifier.h"
// PNCallbackManager类负责Progress Notify的回调管理,包括回调类的注册,回调函数的调用等
class PNCallbackManager
{
public:
voidUpdateProgressStatus(DWORDdwIpAddr, WORDwPort, BYTEbyPercent);
voidRegisterNotifier(IProgressNotifier* pnotifier);
PNCallbackManager();
virtual ~PNCallbackManager();
protected:
IProgressNotifier* m_pnotifier;
};
底下为该类的具体实现代码:
// PNCallbackManager.cpp
#include "stdafx.h"
#include "PNCallbackManager.h"
PNCallbackManagerg_PNCallbackManager;
PNCallbackManager::PNCallbackManager()
{
m_pnotifier = NULL;
}
PNCallbackManager::~PNCallbackManager()
{
}
// 将加载进度更新的具体实现类的指针保存下来,实现注册类的功能
voidPNCallbackManager::RegisterNotifier(IProgressNotifier* pnotifier)
{
m_pnotifier = pnotifier;
}
// 传递更新加载进度的功能调用,实现客户程序与CBB程序的松耦合
voidPNCallbackManager::UpdateProgressStatus(DWORDdwIpAddr, WORDwPort, BYTEbyPercent)
{
if (m_pnotifier != NULL)
m_pnotifier->UpdateProgressStatus(dwIpAddr, wPort, byPercent);
}
总结:
总的来讲,注册回调类与注册回调函数在本质上是相同的,只是思维的角度略有不同,从该实例中,我们看到一个面向过程的C语言世界中问题,在面向对象的世界里如何得到优雅的解决,从某种层面体现出C++多态的强大。虽然,用C++的多态方法实现,在性能上会比注册回调函数的方法会差一些,但毕竟这些服务不是关键执行程序,所以性能不是问题,易用性是要占第一位的。