一,使用抽象基类重用C++对象
在上一篇文章《COM组件开发实践(四)---From C++ to COM :Part 1》中,我们已经将要复用的C++对象封装到DLL中了,对象的声明和实现已经实现了剥离,但还有问题:对象的私有成员(如我们示例中CDB类的数组变量m_arrTables)还是被客户看得一清二楚,即使客户没办法去访问它们;若对象改变了它的数据成员的大小,则所有的客户程序必须重新编译。
而实际上,客户需要的仅仅是对象的成员函数的地址,因此使用抽象基类可以很好地满足以上需求,客户就不会包含对象的私有数据成员,就算对象改变了数据成员的大小,客户程序也不用重新编译。
1. 修改接口文件
首先将接口都改成抽象基类,这是客户程序唯一所需要的代码。具体包括下面几步:1)将CDB和CDBSrvFactory的函数都改成纯虚函数。2)删除数据成员。3)删除所有成员函数的引出标志。4)将CDB改成IDB(表示DB的接口),CDBSrvFactory改成IDBSrvFactory(表示DB类工厂的接口)
typedef
long
HRESULT;
#define
DEF_EXPORT __declspec(dllexport)
class
IDB
{
//
Interfaces
public
:
//
Interface for data access
virtual
HRESULT Read(
short
nTable,
short
nRow, LPWSTR lpszData)
=
0
;
virtual
HRESULT Write(
short
nTable,
short
nRow, LPCWSTR lpszData)
=
0
;
//
Interface for database management
virtual
HRESULT Create(
short
&
nTable, LPCWSTR lpszName)
=
0
;
virtual
HRESULT Delete(
short
nTable)
=
0
;
//
Interfase para obtenber informacion sobre la base de datos
virtual
HRESULT GetNumTables(
short
&
nNumTables)
=
0
;
virtual
HRESULT GetTableName(
short
nTable, LPWSTR lpszName)
=
0
;
virtual
HRESULT GetNumRows(
short
nTable,
short
&
nRows)
=
0
;
virtual
ULONG Release()
=
0
;
};
class
IDBSrvFactory
{
//
Interface
public
:
virtual
HRESULT CreateDB(IDB
**
ppObject)
=
0
;
virtual
ULONG Release()
=
0
;
};
HRESULT DEF_EXPORT DllGetClassFactoryObject(IDBSrvFactory
**
ppObject);
2.修改对象程序
在DLL项目中,我们实现为这个抽象接口声明并实现具体的子类,让CDB从IDB派生,CDBSrvFactory从IDBSrvFactory派生,并且把类工厂CDBSrvFactory的CreateDB方法的参数由CDB**改成IDB**。
#include
"
..\interface\dbsrv.h
"
typedef
long
HRESULT;
class
CDB :
public
IDB
{
//
Interfaces
public
:
//
Interface for data access
HRESULT Read(
short
nTable,
short
nRow, LPWSTR lpszData);
HRESULT Write(
short
nTable,
short
nRow, LPCWSTR lpszData);
//
Interface for database management
HRESULT Create(
short
&
nTable, LPCWSTR lpszName);
HRESULT Delete(
short
nTable);
//
Interfase para obtenber informacion sobre la base de datos
HRESULT GetNumTables(
short
&
nNumTables);
HRESULT GetTableName(
short
nTable, LPWSTR lpszName);
HRESULT GetNumRows(
short
nTable,
short
&
nRows);
ULONG Release();
//
CPPTOCOM: need to free an object in the DLL, since it was allocated here
//
Implementation
private
:
CPtrArray m_arrTables;
//
Array of pointers to CStringArray (the "database")
CStringArray m_arrNames;
//
Array of table names
public
:
~
CDB();
};
class
CDBSrvFactory :
public
IDBSrvFactory
{
//
Interface
public
:
HRESULT CreateDB(IDB
**
ppObject);
ULONG Release();
};
这两个具体子类的实现代码在此就省略不表了,参考上篇文章。
3.修改客户程序
最后根据上面的修改,对客户程序也做相应修改:1)将CDBDoc类的数据成员类型由CDB*改成IDB*。
IDB
*
m_pDB;
//
pointer to database object
2)CDBDoc::OnNewDocument函数中,将CDBSrvFactory*改成IDBSrvFactory*
BOOL CDBDoc::OnNewDocument()
{
…
//
新建数据库对象
//
m_pDB=new CDB;
IDBSrvFactory
*
pDBFactory
=
NULL;
DllGetClassFactoryObject(
&
pDBFactory);
pDBFactory
->
CreateDB(
&
m_pDB);
pDBFactory
->
Release();
//
do not need the factory anymore
//
初始化数据成员变量
…
return
TRUE;
}
OK,最后重新编译DLL即可。可以看出,通过使用虚函数和抽象基类,这才算真正实现了面向接口编程。
二,初步逼近COM--使用COM库加载C++对象
上面的代码中声明了DLL的一个入口点DllGetClassFactoryObject,而客户程序就是通过调用这个函数,从而获取到相应的类工厂对象,再使用类工厂对象创建真正的对象的。我们这一步要尝试让客户程序不去直接调用对象的入口函数,而是让COM库为我们服务,并且要让对象使用标准的入口函数。
1,修改接口定义文件
首先在接口定义文件中增加类ID和接口ID的声明,这两个ID在对象程序和客户程序中都会有定义,在这里只是对这两个外部变量进行说明。然后将引出函数DllGetClassFactoryObject删除,因为接下来我们将会使用标准入口函数DllGetObject,因此不需要再自己定义入口函数了。
typedef
long
HRESULT;
#define
DEF_EXPORT __declspec(dllexport)
//
{30DF3430-0266-11cf-BAA6-00AA003E0EED}
extern
const
GUID CLSID_DBSAMPLE;
//
{ 0x30df3430, 0x266, 0x11cf, { 0xba, 0xa6, 0x0, 0xaa, 0x0, 0x3e, 0xe, 0xed } };
//
{30DF3431-0266-11cf-BAA6-00AA003E0EED}
extern
const
GUID IID_IDBSrvFactory;
//
{ 0x30df3431, 0x266, 0x11cf, { 0xba, 0xa6, 0x0, 0xaa, 0x0, 0x3e, 0xe, 0xed } };
class
IDB
{
//
Interfaces
public
:
//
Interface for data access
virtual
HRESULT Read(
short
nTable,
short
nRow, LPWSTR lpszData)
=
0
;
virtual
HRESULT Write(
short
nTable,
short
nRow, LPCWSTR lpszData)
=
0
;
//
Interface for database management
virtual
HRESULT Create(
short
&
nTable, LPCWSTR lpszName)
=
0
;
virtual
HRESULT Delete(
short
nTable)
=
0
;
//
Interfase para obtenber informacion sobre la base de datos
virtual
HRESULT GetNumTables(
short
&
nNumTables)
=
0
;
virtual
HRESULT GetTableName(
short
nTable, LPWSTR lpszName)
=
0
;
virtual
HRESULT GetNumRows(
short
nTable,
short
&
nRows)
=
0
;
virtual
ULONG Release()
=
0
;
};
class
IDBSrvFactory
{
//
Interface
public
:
virtual
HRESULT CreateDB(IDB
**
ppObject)
=
0
;
virtual
ULONG Release()
=
0
;
};
//
HRESULT DEF_EXPORT DllGetClassFactoryObject(IDBSrvFactory ** ppObject);
2,修改对象程序
修改如下:1)为类ID和接口ID定义GUID。2)将DllGetClassFactoryObject改成标准入口函数DllGetClassObject。具体代码如下:
#include
"
stdafx.h
"
#include
"
DBsrvImp.h
"
//
{30DF3430-0266-11cf-BAA6-00AA003E0EED}
static
const
GUID CLSID_DBSAMPLE
=
{
0x30df3430
,
0x266
,
0x11cf
, {
0xba
,
0xa6
,
0x0
,
0xaa
,
0x0
,
0x3e
,
0xe
,
0xed
} };
//
{30DF3431-0266-11cf-BAA6-00AA003E0EED}
static
const
GUID IID_IDBSrvFactory
=
{
0x30df3431
,
0x266
,
0x11cf
, {
0xba
,
0xa6
,
0x0
,
0xaa
,
0x0
,
0x3e
,
0xe
,
0xed
} };
//
Create a new database object and return a pointer to it
HRESULT CDBSrvFactory::CreateDB(IDB
**
ppvDBObject)
{
*
ppvDBObject
=
(IDB
*
)
new
CDB;
return
NO_ERROR;
}
ULONG CDBSrvFactory::Release()
{
delete
this
;
return
0
;
}
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid,
void
**
ppObject)
{
if
(rclsid
!=
CLSID_DBSAMPLE)
{
return
CLASS_E_CLASSNOTAVAILABLE;
}
if
(riid
!=
IID_IDBSrvFactory)
{
return
E_INVALIDARG;
}
*
ppObject
=
(IDBSrvFactory
*
)
new
CDBSrvFactory;
return
NO_ERROR;
}
注:1)由于标准入口函数DllGetClassObject在 objbase.h文件中已经声明了,因此在我们的代码中不必再声明它。2)在stdafx.h中加入了#include <ole2.h>,并且在所有的include前加入了:#define _AFX_NO_BSTR_SUPPORT,这是因为MFC头文件中的一些定义和ole2.h中不一样。
然后,要让客户程序访问到入口函数,我们要为创建一个模块定义DEF文件,在其中引出DllGetClassObject函数,DB.def代码如下:
EXPORTS
;
WEP @
1
RESIDENTNAME
DllGetClassObject
注:不能用__declspec(dllexport)来引出DllGetClassObject函数,因为在objbase.h中它的定义处已经使用了其他修饰词。
最后,这个对象要想通过COM库创建,就必须在注册表中进行注册,但目前还没有加入自我注册部分,因此我们先对其进行手动注册吧。其实要做的事情很简单,就是把我们的DLL的路径告诉给COM库就行了。步骤如下:
1)HKEY_CLASSES_ROOT"CLSID下添加一个子键,名字就是上面定义的类ID:{ 30DF3430-0266-11cf-BAA6-00AA003E0EED}。
2)为这个子键再添加一个子键InprocServer32,为它添加一个未命名的字符串值:类型为REG_SZ,数据为<path>"db.dll,也就是保存你的DLL的路径。
3,修改客户程序
1)在DBDoc.cpp中也加入类ID和接口ID的定义
//
{30DF3430-0266-11cf-BAA6-00AA003E0EED}
static
const
GUID CLSID_DBSAMPLE
=
{
0x30df3430
,
0x266
,
0x11cf
, {
0xba
,
0xa6
,
0x0
,
0xaa
,
0x0
,
0x3e
,
0xe
,
0xed
} };
//
{30DF3431-0266-11cf-BAA6-00AA003E0EED}
static
const
GUID IID_IDBSrvFactory
=
{
0x30df3431
,
0x266
,
0x11cf
, {
0xba
,
0xa6
,
0x0
,
0xaa
,
0x0
,
0x3e
,
0xe
,
0xed
} };
2)初始化COM库。在我们使用COM库之前先对其进行初始化。
if
(FAILED(CoInitialize(NULL)))
{
AfxMessageBox(_T(
"
Could not initialize COM Libraries!
"
));
return
FALSE;
}
并且在程序退出时调用CoUninitialize()
int
CDBApp::ExitInstance()
{
CoUninitialize();
return
CWinApp::ExitInstance();
}
2)改为使用COM库函数来创建对象。在CDBDoc::OnNewDocument()函数中,使用COM库函数CoGetClassObject代替原来直接装载DLL的方式。
//
新建数据库对象
//
m_pDB=new CDB;
IDBSrvFactory
*
pDBFactory
=
NULL;
HRESULT hRes;
hRes
=
CoGetClassObject(CLSID_DBSAMPLE, CLSCTX_SERVER, NULL, IID_IDBSrvFactory, (
void
**
)
&
pDBFactory);
if
(FAILED(hRes))
{
CString csError;
csError.Format(_T(
"
Error %x creating DB Object!
"
), hRes);
AfxMessageBox(csError);
return
FALSE;
}
pDBFactory
->
CreateDB(
&
m_pDB);
pDBFactory
->
Release();
//
do not need the factory anymore
注:以前在客户程序中需要对应的DLL在路径下,但现在并不需要了,因为COM库会根据注册表中的信息找到DLL所在路径的。