在用C++写要导出类的库时,我们经常只想暴露接口,而隐藏类的实现细节。也就是说我们提供的头文件里只提供要暴露的公共成员函数的声明,类的其他所有信息都不会在这个头文件里面显示出来。这个时候就要用到接口与实现分离的技术。《COM技术内幕》 §7 —— 类厂、COM库
CoCreateInstance的声明:
HRESULT __stdcall CoCreateInstance(
const CLSID& clsid,
IUnknown* pIUnknownOuter,
DWORD dwClsContext,
const IID& iid,
void** ppv
);
CoCreateInstance有四个输入参数和一个输出参数。第一个参数是待创建组件的CLSID。第二个参数是用于聚合组件的。第三个参数的作用是限定所创建的组件的执行上下文。第四个参数iid为组件上待使用的接口的IID。CoCreateInstance将在最后一个参数中返回此接口的指针。通过将一个IID传给CoCreateInstance,客户将无需在创建组件之后去调用其QueryInterface函数。
CoCreateInstance的使用:
// Create Component
IX* pIX = NULL;
HRESULT hr = :: CoCreateInstance( CLSID_Component1,
NULL,
CLSCTX_INPROC_SERVER,
IID_IX,
(void**)&pIX);
If(SUCCEEDED(hr))
{
pIX->Fx();
pIX->Release();
}
传给CoCreateInstance的最后两个参数同传给QueryInterface的参数是一样的。
CoCreateInstance的第三个参数dwClsContext(类上下文)可以控制所创建的组件是在与客户相同的进程中运行,还是在不同的进程中运行,或者是在另外一台机器上运行。其取值为下列各值的组合:
CLSCTX_INPROC_SERVER
客户希望创建在同一进程中运行的组件。为能够同客户在同一进程中运行,组建必须是在DLL中实现的。
CLSCTX_INPROC_HANDLER
客户希望创建进程中处理器。一个进程甲处理器实际上是一个只实现了某个组件一部分的进程中组件。该组件的其他部分将由本地或远程服务器上的某个进程外组件实现。
CLSCTX_LOCAL_SERVER
客户希望创建一个在同一机器上的另外一个进程中运行的组件。本地服务器是由EXE实现的。
CLSCTX_REMOTE_SERVER
客户希望创建一个在远程机器上运行的组件。此标志需要分布式COM正常工作。
与《COM原理与应用》中第三章的例子程序相对比,《COM技术内幕》第七章的的程序的共同的部分:
(1)客户程序(有待改进):
int main()
{
// CoInitialize();
// CoCreateInstance();
// CoUninitialize();
}
(2)COM程序
// Interfaces Definition
// Component Class Declaration
// Component Class Functions Implementation for IUnknown Interfaces
// {
// QueryInterface(const IID& iid, void** ppv);
// AddRef( );
// Release( );
// }
// Component Class Functions Implementation for IClassFactory Interfaces
// {
// IUnknown Interface
// QueryInterface(const IID& iid, void** ppv);
// AddRef( );
// Release( );
// IClassFactory Interface
// CreateInstance (IUnknown *pUnknownOuter, const IID& iid, void **ppv);
// LockServer(BOOL bLock);
// }
// Exported functions
// DllCanUnloadNow( ); // Can DLL Unload Now?
// DllGetClassObject(const CLSID& clsid, const IID& iid, void **ppv); //Get Class Factory
// Server Registation
// DllRegisterServer( );
// Server Unregistation
// DllUnregisterServer( );
// DLL Module Information
// DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved );
下面用一个最简单的例子来说明。
类ClxExp是我们要导出的类,其中有一个私有成员变量是ClxTest类的对象,各个文件内容如下:
lxTest.h文件内容:
class ClxTest
{
public:
ClxTest();
virtual ~ClxTest();
void DoSomething();
};
lxTest.cpp文件内容:
#include "lxTest.h"
#include <iostream>
using namespace std;
ClxTest::ClxTest()
{
}
ClxTest::~ClxTest()
{
}
void ClxTest::DoSomething()
{
cout << "Do something in class ClxTest!" << endl;
}
lxExp.h文件内容:
#include "lxTest.h"
class ClxExp
{
public:
ClxExp();
virtual ~ClxExp();
void DoSomething();
private:
ClxTest m_lxTest;
void lxTest();
};
lxExp.cpp文件内容:
#include "lxExp.h"
ClxExp::ClxExp()
{
}
ClxExp::~ClxExp()
{
}
// 其实该方法在这里并没有必要,我这样只是为了说明调用关系
void ClxExp::lxTest()
{
m_lxTest.DoSomething();
}
void ClxExp::DoSomething()
{
lxTest();
}
为了让用户能使用我们的类ClxExp,我们必须提供lxExp.h文件,这样类ClxExp的私有成员也暴露给用户了。而且,仅仅提供lxExp.h文件是不够的,因为lxExp.h文件include了lxTest.h文件,在这种情况下,我们还要提供lxTest.h文件。那样ClxExp类的实现细节就全暴露给用户了。另外,当我们对类ClxTest做了修改(如添加或删除一些成员变量或方法)时,我们还要给用户更新lxTest.h文件,而这个文件是跟接口无关的。如果类ClxExp里面有很多像m_lxTest那样的对象的话,我们就要给用户提供N个像lxTest.h那样的头文件,而且其中任何一个类有改动,我们都要给用户更新头文件。还有一点就是用户在这种情况下必须进行重新编译!上面是非常小的一个例子,重新编译的时间可以忽略不计。但是,如果类ClxExp被用户大量使用的话,那么在一个大项目中,重新编译的时候我们就有时间可以去喝杯咖啡什么的了。当然上面的种种情况不是我们想看到的!你也可以想像一下用户在自己程序不用改动的情况下要不停的更新头文件和编译时,他们心里会骂些什么。其实对用户来说,他们只关心类ClxExp的接口DoSomething()方法。那我们怎么才能只暴露类ClxExp的DoSomething()方法而不又产生上面所说的那些问题呢?答案就是--接口与实现的分离。我可以让类ClxExp定义接口,而把实现放在另外一个类里面。下面是具体的方法:
首先,添加一个实现类ClxImplement来实现ClxExp的所有功能。注意:类ClxImplement有着跟类ClxExp一样的公有成员函数,因为他们的接口要完全一致。
lxImplement.h文件内容:
#include "lxTest.h"
class ClxImplement
{
public:
ClxImplement();
~ClxImplement();
void DoSomething();
private:
ClxTest m_lxTest;
void lxTest();
};
lxImplement.cpp文件内容:
#include "lxImplement.h"
ClxImplement::ClxImplement()
{
}
ClxImplement::~ClxImplement()
{
}
void ClxImplement::lxTest()
{
m_lxTest.DoSomething();
}
void ClxImplement::DoSomething()
{
lxTest();
}
然后,修改类ClxExp。
修改后的lxExp.h文件内容:
// 前置声明
class ClxImplement;
class ClxExp
{
public:
ClxExp();
virtual ~ClxExp();
void DoSomething();
private:
// 声明一个类ClxImplement的指针,不需要知道类ClxImplement的定义
ClxImplement *m_pImpl;
};
修改后的lxExp.cpp文件内容:
// 在这里包含类ClxImplement的定义头文件
#include "lxImplement.h"
ClxExp::ClxExp()
{
m_pImpl = new ClxImplement;
}
ClxExp::~ClxExp()
{
if (m_pImpl)
delete m_pImpl;
}
void ClxExp::DoSomething()
{
m_pImpl->DoSomething();
}
通过上面的方法就实现了类ClxExp的接口与实现的分离。请注意两个文件中的注释。类ClxExp里面声明的只是接口而已,而真正的实现细节被隐藏到了类ClxImplement里面。为了能在类ClxExp中使用类ClxImplement而不include头文件lxImplement.h,就必须有前置声明class ClxImplement,而且只能使用指向类ClxImplement对象的指针,否则就不能通过编译。在发布库文件的时候,我们只需给用户提供一个头文件lxExp.h就行了,不会暴露类ClxExp的任何实现细节。而且我们对类ClxTest的任何改动,都不需要再给用户更新头文件(当然,库文件是要更新的,但是这种情况下用户也不用重新编译!)。这样做还有一个好处就是,可以在分析阶段由系统分析员或者高级程序员来先把类的接口定义好,甚至可以把接口代码写好(例如上面修改后的lxExp.h文件和lxExp.cpp文件),而把类的具体实现交给其他程序员开发。
评论:
这是接口-实现分离的一种方法,也叫做句柄类,还有一个更好的方法是通过声明一个抽象类做为接口,实现这个接口,使用更简单,易于理解。
class ISomething {
public:
virtual int Method1() = 0;
virtual int Method2() = 0;
};
class CSthImpl : public ISomething {
public:
// ISomething methods
int Method1();
int Method2();
public:
CSthImpl();
~CSthImpl();
private:
...
};
更喜欢Iface 的方法
事实上,COM的基本原理就是这样,只是它用的是interface关键字,事实上就是struct,本质上就是只含有虚方法的抽象类。
对于小型工程来说, 这么用其实很不划算, 一旦接口发生变更, 你需要修改:
实现类
句柄类
接口类
如果一定要把接口做的很干净, 不如直接用抽象基类, 这样接口变化时只要修改接口类和抽象基类
如果只是希望接口类不依赖其他头文件, 那在接口类中所有对象都动态生成就可以了
class ClxTest;
class ClxExp
{
public:
ClxExp();
virtual ~ClxExp();
void DoSomething();
private:
ClxTest *m_plxTest;
void lxTest();
};
自己的一点感觉, 反正我很少用这种句柄类的设计
我没弄明白ClxImplement类的作用, |
2007-11-17 15:32:16作者回复: |