6.COM可重用性——聚合

本次讲COM的另一大可重用特性——聚合,前文讲了包容其实相当于客户程序的角色,而本文说的聚合则完全体现了可重用性。

1.聚合原理

如下图(借用《COM原理与应用》)


对象B只实现了接口IOtherInterface,但是当你向对象B请求ISomeInterface时,对象B直接把对象A的ISomeInterface暴露给客户程序,调用ISomeInterface工作时和对象B没有任何关系,这就是聚合的过程,顾名思义可以把各种接口简单的聚合到此处使用。

但是并不是所有对象都能被聚合,聚合过程必须对应的对象支持才行

以此处对象A和对象B为例,虽然对象B聚合了对象A的ISomInterface,然而在客户程序看来,并不知道从对象B处拿到的ISomeInterface实际上是对象A的接口,这一过程对于客户程序是不可见的。这就涉及到一个问题,用户认为ISomeInterface是对象B的,自然想从ISomeInterface查询对象B的其他接口,如IOtherInterface,而此时ISomeInterface是对象A的,显然就无法直接完成这一过程了,这就是矛盾所在。

那要怎么办呢,好说!我们在请求对象A的ISomeInterface接口时,告诉它这个接口是给外部聚合用的,这样从ISomeInterface查询对象B的接口时,直接调用对象B来完成即可。

具体CoCreateInstance原型如下

STDAPI CoCreateInstance(
  REFCLSID rclsid,
  LPUNKNOWN pUnkOuter,
  DWORD dwClsContext,
  REFIID riid,
  LPVOID * ppv
);

可以看到,这里可以传入一个pUnkOuter参数,这个参数表明了是哪个外部对象在聚合本对象,为NULL时表明没有被聚合。一旦告诉A是对象B在聚合这个对象,通过A对象的ISomeInterface查询对象B的其他接口时,就可以直接调用对象B来完成

2.实现原理

如下图



假设对象B聚合了对象A的IName接口。

实现对象A时指明一个委托IUnknown和一个非委托IUnknown,非委托IUnknown完成之前IUnknown接口工作,委托IUnknown则根据传入的pUnkOuter参数判断是调用非委托IUnknown接口还是pUnkOuter接口完成具体工作。具体如下:

1.对象B中查找A的IUnknown接口时,一定是A的非委托IUnknown接口,通过此IUnknown接口可以获得A的IName接口。

2.通过已获得的IName接口查找其它接口时,会通过委托IUnkown判断当前是否出于聚合状态,从而决定是调用非聚合IUnkown接口查询还是调用传入的B的IUnkown接口来查询。


3.虚函数和多重继承

在具体实现聚合程序前,需要先搞懂虚函数和多重继承的特性,如下程序

class IInterfaceA
{
public:
	virtual void A_PrintAge(int age) = 0;
};

class IInterfaceAA : public IInterfaceA
{
};

class IInterfaceB
{
public:
	virtual void B_PrintAge(int age) = 0;
};

class IInterfaceBB : public IInterfaceB
{
};

class TestPrint: public IInterfaceAA, public IInterfaceBB
{
public:
	virtual void A_PrintAge(int age)
	{
		cout << __FUNCTION__ << " My Age is: " << age << endl;
	}

	virtual void B_PrintAge(int age)
	{
		cout << __FUNCTION__ << " My Age is: " << age << endl;
	}
};
具体的类继承结构如下:

IInterfaceA		IInterfaceB
|				|
InterfaceAA		InterfaceBB
|_______________|
		|
		TestPrint

TestPrint中存在两个虚表,具体 使用接口指针接对应对象指针时会命中其中一个分支,调用对应接口功能

如下两个分别命中IInterfaceA和IInterfaceB功能

IInterfaceA* pA = (IInterfaceA*)p;		    //命中IInterfaceA分支
pA->A_PrintAge(10);

IInterfaceAA* pAA = (IInterfaceAA*)p;	    //命中IInterfaceA分支
pAA->A_PrintAge(10);

cout << endl;

IInterfaceB* pB1 = (IInterfaceB*)p;			//命中IInterfaceB分支
pB1->B_PrintAge(10);

IInterfaceBB* pBB1 = (IInterfaceBB*)p;		//命中IInterfaceB分支
pBB1->B_PrintAge(10);

但是这里还存在一个重要概念, 相同的接口结构,只要虚表结构(参数类型、个数、返回值)相同,就可以互换,记住这个特性,这是实现 聚合的关键,如下

	TestPrint *p = new TestPrint;

	IInterfaceA* pA = (IInterfaceA*)p;		//命中IInterfaceA分支
	pA->A_PrintAge(10);
	IInterfaceB* pB = (IInterfaceB*)pA;		//IInterfaceA虚表转成IInterfaceB虚表
	pB->B_PrintAge(10);

	IInterfaceAA* pAA = (IInterfaceAA*)p;	//命中IInterfaceA分支
	pAA->A_PrintAge(10);
	IInterfaceBB* pBB = (IInterfaceBB*)pAA;	//IInterfaceAA虚表转成IInterfaceBB虚表
	pBB->B_PrintAge(10);
	pB = (IInterfaceB*)pAA;					//IInterfaceAA虚表转成IInterfaceB虚表
	pB->B_PrintAge(10);

	cout << endl;
	IInterfaceB* pB1 = (IInterfaceB*)p;			//命中IInterfaceB分支
	pB1->B_PrintAge(10);
	IInterfaceA* pA1 = (IInterfaceA*)pB1;		//IInterfaceB虚表转成IInterfaceA虚表
	pA1->A_PrintAge(10);

	IInterfaceBB* pBB1 = (IInterfaceBB*)p;		//命中IInterfaceB分支
	pBB1->B_PrintAge(10);
	IInterfaceAA* pAA1 = (IInterfaceAA*)pBB1;	//IInterfaceBB虚表转成IInterfaceAA虚表
	pAA1->A_PrintAge(10);
	pA1 = (IInterfaceA*)pBB1;					//IInterfaceBB虚表转成IInterfaceA虚表
	pA1->A_PrintAge(10);

IInterfaceA和IInterfaceB的*_PrintAge函数有相同的接口,因此 可以用IInterfaceB的接口去承接IInterfaceA的内容,此时调用B_PrintAge其实就是调用IInterfaceA->A_PrintAge

4.聚合的实现

还是用之前的例子,这里对象B只实现IName接口,当客户程序查询IAge接口时,直接返回对象A的IAge接口。

先改造对象A支持聚合

1.定义非委托IUnknown接口

如下

//非委托IUnknown接口
class INondelegationUnknown
{
public:
	virtual HRESULT STDMETHODCALLTYPE NondelegationQueryInterface( _In_ REFIID riid, _Out_ void **ppvObject) = 0;
	virtual ULONG STDMETHODCALLTYPE NondelegationAddRef( void) = 0;
	virtual ULONG STDMETHODCALLTYPE NondelegationRelease( void) = 0;
};
它的结构和IUnknown结构一样,因此可以用IUnknown指针来接收它的指针。

2.委托IUnknown接口实现

创建对象A时记下外部IUnknown接口对象,

CPeople::CPeople(IUnknown* pUnknownOuter)
{
	m_pUnknownOuter = pUnknownOuter;

	g_EasyComNumber++;//组件引用计数
	this->m_nRef = 0;
}

在委托接口中进行判断选择,m_pUnknownOuter为NULL则调用非委托接口,不为NULL则调用m_pUnknownOuter外部接口

//IUnknown 委托接口
HRESULT STDMETHODCALLTYPE CPeople::QueryInterface( _In_ REFIID riid, _Out_ void **ppvObject )
{
	if (NULL != m_pUnknownOuter)
	{
		return m_pUnknownOuter->QueryInterface(riid, ppvObject);
	}
	else
	{
		return NondelegationQueryInterface(riid, ppvObject);
	}
}

ULONG STDMETHODCALLTYPE CPeople::AddRef( void )
{
	if (NULL != m_pUnknownOuter)
	{
		return m_pUnknownOuter->AddRef();
	}
	else
	{
		return NondelegationAddRef();
	}
}

ULONG STDMETHODCALLTYPE CPeople::Release( void )
{
	if (NULL != m_pUnknownOuter)
	{
		return m_pUnknownOuter->Release();
	}
	else
	{
		return NondelegationRelease();
	}
}

3.非委托IUnknown接口实现

HRESULT STDMETHODCALLTYPE CPeople::NondelegationQueryInterface( _In_ REFIID riid, _Out_ void **ppvObject )
{
	if (riid == IID_IUnknown)
	{
		*ppvObject = (INondelegationUnknown*)this;//强制返回的是非委托IUnknown接口,注意虚函数和多重继承的使用
		((INondelegationUnknown*)this)->NondelegationAddRef();
	}
	else if (riid == IID_IAge)
	{
		*ppvObject = (IAge*)this;//委托IUnknown接口
		((IAge*)this)->AddRef();
	}
	else if (riid == IID_IName)
	{
		*ppvObject = (IName*)this;//委托IUnknown接口
		((IName*)this)->AddRef();
	}
	else
	{
		*ppvObject = NULL;
		return E_NOINTERFACE;
	}

	return S_OK;
}

ULONG STDMETHODCALLTYPE CPeople::NondelegationAddRef( void )
{
	m_nRef++;

	return (ULONG)m_nRef;
}

ULONG STDMETHODCALLTYPE CPeople::NondelegationRelease( void )
{
	m_nRef--;
    
	if (m_nRef == 0)
	{
		delete this;
		return 0;
	}

	return (ULONG)m_nRef;
}

这里AddRef和Release是正常IUnknown实现, 最关键的在于这里的非委托QueryInterface的实现,注意如下几点:

1.查询IUnknown接口时强制返回的是非委托IUnknown接口,增加的引用计数也是自己的

2.查询其它接口时返回的对象支持的接口都是委托IUnknown接口,增加的引用计数也是调用委托AddRef进行的

4.创建函数

//IClassFactory
HRESULT STDMETHODCALLTYPE CPeopleFactory::CreateInstance( _In_ IUnknown *pUnkOuter, _In_ REFIID riid, _Out_ void **ppvObject )
{
	if (NULL!=pUnkOuter && IID_IUnknown!=riid)
	{
		return CLASS_E_NOAGGREGATION;
	}

	CPeople *pObj = NULL;
	HRESULT hr = S_FALSE;

	*ppvObject = NULL;

	//创建组件对象
	pObj = new CPeople(pUnkOuter);
	if (pObj == NULL)
	{
		return hr;
	}

	//获得非托管第一个接口指针
	hr = pObj->NondelegationQueryInterface(riid, ppvObject);
	if (S_OK != hr)
	{
		delete pObj;
	}

	return hr;
}
这里注意两点:

1.外部聚合本对象时,只能传入pUnkOuter的同时查询IUnknown接口,这样做是为了实现方便

2.此处使用的非委托NondelegationQueryInterface查询非委托IUnknown接口,外部仍然可以用IUnknown接口接收,这是前面讲的虚函数的特性,不不再赘述

然后,对象B中实现对象A的初始化等工作如下

1.对象A的初始化

HRESULT CPeople::Init()
{
	IUnknown* pUnknownOuter = (IUnknown*)this;
	HRESULT hr = CoCreateInstance(CLSID_EasyComPeople, pUnknownOuter, CLSCTX_INPROC_SERVER, IID_IUnknown, (LPVOID*)&m_pUnknownInner);

	if (FAILED(hr))
	{
		std::cout << "Create EasyCom Fail In EasyComEx2" << endl;
		return E_FAIL;
	}

	//本次聚合IAge接口,因此尝试查询IAge接口是否存在
	IAge* pAge = NULL;
	hr = m_pUnknownInner->QueryInterface(IID_IAge, (LPVOID*)&pAge);
	if (FAILED(hr))
	{
		m_pUnknownInner->Release();
		return E_FAIL;
	}

	pAge->Release();  //此处查询IID_IAge接口,调用的是[CLSID_EasyComPeople]委托IUnknown接口,
					  //判断传入的pUnknownOuer非NULL,故调用外部AddRef,这里pAge Release也是调用的外部Release接口
					  //在潘爱民的《COM原理与应用》类似实现此处直接使用pUnknownOuter->Release,容易造成误解
	return S_OK;
}
这里得到对象A的IUnknown指针, pAge接口AddRef和Release调用的都是外部对象的

2.对象B查询IAge接口时

if (riid == IID_IAge)
{
    return  m_pUnknownInner->QueryInterface(IID_IAge, ppvObject);
}
直接查询返回对象A的IAge接口,而且由于此时IAge接口位于委托IUnknown分支上,查询其他接口时判断pUnknownOuter非NULL,会再调用对象B即pUnknownOuter查询接口

下面看看何时调用初始化和析构

3.工厂对象中调用初始化

//IClassFactory
HRESULT STDMETHODCALLTYPE CPeopleFactory::CreateInstance( _In_ IUnknown *pUnkOuter, _In_ REFIID riid, _Out_ void **ppvObject )
{
	CPeople *pObj = NULL;
	HRESULT hr = S_FALSE;

	*ppvObject = NULL;

	//创建组件对象
	pObj = new CPeople;
	if (pObj == NULL)
	{
		return hr;
	}

	//Init调用前引用计数为0,Init中先AddRef后Release会导致对象被销毁,所以此处先AddRef再Init最后再Release
	//这是COM引用计数的常用方法
	pObj->AddRef();

	//初始化包容对象
	hr  = pObj->Init();
	if (FAILED(hr))
	{
		std::cout << "EasyComEx2 Init Fail" << std::endl;
		delete pObj;
		return hr;
	}

	//获得非托管第一个接口指针
	hr = pObj->QueryInterface(riid, ppvObject);
	if (S_OK != hr)
	{
		delete pObj;
	}

	pObj->Release();

	return hr;
}

4.释放对象A接口

CPeople::~CPeople()
{
	g_EasyComNumber--;//组件引用计数

	//同样遵循COM使用规则,因为m_pAge->Release会调用外部Release,这里先AddRef,然后再Release
	//然而此处计数已经为0了,AddRef后再m_pAge->Release会导致引用计数为0,再次delete,造成递归
	//所以只好先将引用计数强制指定为1,反正马上对象就要销毁了,设成多少也无所谓了,也不用Release了
	//这里是《COM原理与应用》书上用到代码,其实m_pAge压根就用不到,因为直接通过m_pUnknownInner查询了,因此此段代码实属多余
	/*m_nRef = 1;
	IUnknown* pUnknownOuter = this;
	pUnknownOuter->AddRef();

	if (m_pAge!=NULL)
	{
		m_pAge->Release();
	}*/

	if (m_pUnknownInner != NULL)
	{
		m_pUnknownInner->Release();
	}
}
析构函数中只需简单的Release对象A接口即可,这里《COM原理与应用》将此处复杂化了,详细看注释。

本文完整演示代码下载链接

原创,转载请注明来自http://blog.csdn.net/wenzhou1219

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值