简单接口实现规范
简单接口实现规范
作者:Softit
增补:小小企鹅,StoneLee
最新更新:2003-5-27
预备知识:
●C++的基础概念,特别是虚函数和多态
●COM,建议参考书籍《COM 本质论》(ISBN:7-5083-0611-2)
第一章整体概念
第一节概要说明
基于组件的软件设计方法是软件工业实践的一个基本成功经验,在软件设计过程中要考虑模块的少耦合少依赖,这是模块重用的基础。C++虚函数为接口提供了理论基础。之所以称之为“简单接口”,是相对于COM和CORBA组件而言,大部分小组件不需要支持引用计数、多语言开发、跨网络运行等特性。运用简单接口还可以很容易写出模块化的插件,例如,可以将棋牌类客户端做成插件形式,但外观可以使用公用的界面框架,也可以嵌入到游戏大厅里。简单接口实现的组件将来改造成ActiveX组件也很容易。
第二节名词解释
一、图示
二、说明
1、接口
一组纯虚函数的集合。
实现时,是个头文件,里面全部是纯虚函数,从C++观点讲,就是一个函数指针表(vfnTable),详细可参考COM有关书籍。
例如,上图中的IFoo部分。
2、服务
实现接口的组件,供客户应用程序调用,我们称此组件提供了一个支持接口的服务,或简单理解成Server也可以。
服务一般以DLL或lib库和接口的头文件一起提供。(当然:最好还应该有一个说明文档)。
例如,上图中的CFoo部分。
3、客户
使用接口的程序,一般是调用接口的具体应用程序,也可理解为Client。
一般客户都是独立成为一个应用程序。
如上图所示,为CExtern部分。
4、回调接口
有的时候,客户通过接口调用服务的相关方法后,需要知道这些方法是否执行成功。但是存在下面两种可能:
1)由于服务可能是异步模式,所以客户并不能马上通过方法的返回值获得。
2)或则,为了程序的结构清晰,服务并不想通过接口的调用的返回值,而是希望通过调用客户的一些固定的函数来通知客户事件发生。
这时,就需要用到回调接口。
和接口不同,回调接口实际是服务发起的,由客户实现的。而接口却是有客户发起的,由服务实现的。
接口的使用,是为了把定义与实现分离,这样能提高程序各模块之间的独立性。
类似于COM组件的连接点。“服务”有些事件要通知“客户”,通过回掉接口实现。
回调接口也是用头文件实现,里面是纯虚函数。
客户必须继承和实现这个回调接口。然后将实现回调接口的对象的指针传给服务(一般在服务创建函数中),这样服务就可以在一些约定好的事件中回调客户的程序代码。
如上图所示,CFoo通过回调接口(IFooSink)调用外部模块CExtern的函数,CExtern 实现IFooSink。CExtern实例化后,将实例后的指针传给CFoo。一般定义为以Sink为结束的接口,如IFooSink,表明此接口是供IFoo回调的。
旁注:Sink的英文意思是“接收器”。
5、多接口和多回调接口
I、多接口
任何一个客户,都可能用到多个服务。
比如:我们有一个自动下载的客户程序,CAutoDownLoad,它要使用以下接口为其服务,包括通信接口ICommunicate、资源接口IResMgr。这样,我们在CAutoDownLoad里,只有获得ICommunicate和IResMgr两个指针,就可以通过其实例(即服务对象)实现相关的接口功能。
II、多回调接口
同时,一个客户,也可能实现多个回调接口。
比如:一个游戏服务器CGameServer,被多个回调接口触发,比如:通信回调接口ICmmSink,数据库完成通知IDBSink。
这时,CGameServer只要简单地继承这几个回调接口,如下:
Class CGameServer:public ICmmSink,IDBSink {
}
然后,CGameServer将自己的实例化后的指针,分别传给对应的通信和数据库服务,让他们回调自己。
第三节模块之间通信方式比较:基于接口和基于类共享的方法的比较
简单接口间当一个模块IFoo需要回调其它模块(IExtern)的函数时,在IExtern模块中实现回调接口(IFooSink,IExtern继承接口IFooSink即可),将IFooSink传送给IFoo,这种方式是两个接口间交流的方式,约束简明。接口方式调用的另一个极重要的好处是多态性,即一个接口可以有多种不同的实现方法,如一个CArchieve所包含的月报表数据,通过显示接口IView,可以对应三种不同的显示实现方式(柱状、饼状、数字),而且用户可以在运行时刻动态选择其中的一种或几种显示方式。
相反,如果要调用C++类CFoo的一个方法,需包含CFoo的头定义,头文件中可能有大量私有的,与类之间通讯无关的东西,如CFoo可能包含了很多其它头文件、很多自定义宏等。CFoo不知有哪些个方法被别人调用、有哪些public属性被引用,因此不能轻易修改自已函数,则将两个类的实现绑定在一起,因无法知道一个类调用了另一个类的哪些方法和引用了哪一些public成员,因而类的定义不能轻易修改,否则可能会影响很多模块。
第二章接口的具体实现
第一节接口定义样板
文件:IFoo.h
class IFoo
{
virtual void Release() = NULL; // 释放对象,见下面的说明“接口对象的销毁”
virtual BOOL Add(LPCSTR szName, DWORD dwReserved=0) = NULL;
virtual BOOL Delete(LPCSTR szName) = NULL;
}
接口是一经发布尽可能少修改,所以一些将来可能会修改或一些重要的虚函数定义时要加一个dwReserved参数,便于将来扩充
第二节接口对象的创建:即服务的实例化
一、方法
接口对象的创建即new一个实现此接口的对象,将此对象的接口指针返回即可。创建将来可能更改接口实现时,需要支持版本控制,如互联网应用程序一般要求后期发布的程序与早期发布的模块接口兼容。
二、范例
一般我们将接口实现放在一个DLL中,然后,通过DLL的输出函数来实例化接口对象。
如下面所示意:
1、DLL实例化
一般写在IFoo.cpp中
extern "C" __cdecl__declspec(dllexport) BOOL CreateFooObject(/*out*/IFoo** ppFoo)
{
if(ppFoo == NULL) // 先判断传入指针是否为空,是编码的好习惯return FALSE;
CFoo *pFoo = new CFoo(); //这里实例化服务CFoo
if(pFoo == NULL) // 好习惯
return FALSE;
// 注意:通过类型转换,将CFoo转为IFoo返回给客户
*ppFoo = static_cast
return TRUE;
}
2、服务的实现:Server.dsp
I、Foo.h
class CFoo: public IFoo {
private:
CNameList m_listName; // 假设CNameList类是一个动态数组,
// 支持Insert、RemoveCurrent、Top、GetCurrent、Next等方法public:
virtual void Release();
virtual BOOL AddName(LPCSTR szName, DWORD dwReserved);
virtual BOOL DeleteName(LPCSTR szName);
}
II、Foo.cpp
#include “IFoo.h”
#include “Foo.h”
BOOL CFoo::AddName(LPCSTR szName, DWORD dwReserved)
{
return(m_listName.Insert(szName));
}
BOOL CFoo::DeleteName(LPCSTR szName)
{
m_listName.Top();
BOOL bFind = FALSE;
char *szListName;
while(szListName = m_listName.GetCurrent())
{
if (strcmp(szListName, szName) == 0) {
bFind = TRUE;
m_listName.RemoveCurrent();
}
else
m_listName.Next();
}
return bFind;
}
void CFoo::Release()
{
m_listName.Top();
while(m_listName.GetCurrent()) m_listName.RemoveCurrent();
delete this;
}
3、客户的使用:Client.dsp
I、Extern.h
class CExtern
{
private:
IFoo* m_pFoo;
public:
CExtern() {m_pFoo = NULL;}
void CreateIFoo();
void UseIFoo();
void NotUserIFooNever();
}
II、Extern.cpp
#include “../Include/IFoo.h”// 后面有一章“一些规范”里会解释这个路径
#include “Extern.h”
void CExtern::CreateIFoo()
{
。。。// 为阅读,省略了一些加栽dll的代码
if (!CreateFoolObject(&m_pFoo);
m_pFoo = NULL;
}
void CExtern::UseIFoo()
{
if (m_pFoo)
m_pFoo->AddName(“Test”);
}
void CExtern::NotUseIFooNever()
{
if (m_pFoo)
{
m_pFoo->Release();
m_pFoo = NULL;
}
}
三、先实例化服务,再实例化客户
有时,先实例化服务,然后再实例化客户。例如:我们的游戏服务器和游戏框架,就是采用这种方式。此时,由服务调用客户的创建函数,并将自己的接口指针做为参数传给客户。