聚合的概念
聚合源自組件重用。當(dāng)有兩個組件A和B,他們分別實現(xiàn)了自己的接口IA和IB。如果有一個客戶程序創(chuàng)建了A對象使得自己可以調(diào)用IA的方法,但同時又想獲得IB的接口,調(diào)用IB的方法。這時候有兩種做法:一種是客戶程序創(chuàng)建B對象,還有一種方法是A組件內(nèi)部創(chuàng)建B組件,然后客戶通過某種途徑調(diào)用B的接口方法。
第一種方法,使得客戶必須知道有獨立的B組件的存在,第二種方法客戶可以認為只有一個組件A,組件A實現(xiàn)了兩個接口IA和IB。第二種方法可以制造出一種假象,讓客戶程序編寫更加簡單。從組件A如何管理組件B的方法上,第二種方法還可以分為兩種:包容和聚合。
包容很簡單,如果組件IB接口擁有一個方法F(),那么A組件就要實現(xiàn)一個自己的IBEx接口,并實現(xiàn)IBEx::F( )方法,內(nèi)部調(diào)用IB::F()方法。這樣,客戶也就可以通過調(diào)用IBEx::F()來調(diào)用IB::F。在這種情況下,客戶只知道有IA和IBEx接口,不知道還存在另一個B組件和IB接口。IBEx::F()增加一些代碼從而修改IB::F()方法的功能,甚至可以完全丟棄IB::F()方法。
聚合通常用于IB接口的功能完全不需要做任何的修改,就可以直接交給用戶使用的情況。這時候,如果IB接口的方法很多,包容就顯得很笨拙。因為它不得不對每一個方法作一次包裝,盡管什么都不做。COM+對象池就是通過聚合我們的組件,來把我們組件的接口暴露給客戶的。聚合方式下,A組件直接將IB接口交給客戶,客戶就可以調(diào)用,但是客戶仍然以為是A組件實現(xiàn)了IB接口。如下圖:
組件A
IUnknown
客戶直接使用
客戶直接使用
內(nèi)部組件的實現(xiàn)
客戶程序只知道A組件而不知道B組件,并且認為A組件實現(xiàn)了IA和IB接口。因此,當(dāng)客戶創(chuàng)建了A組件(通過CoCreateInstance函數(shù)),獲取到IUnknown接口時,應(yīng)該獲得的是A組件實現(xiàn)的IUnknown接口。 問題在于B組件有自己的IUnknown的接口實現(xiàn),如果B組件還是采用一般的方法實現(xiàn)IUnknown接口的話,當(dāng)客戶調(diào)用IB::QueryInterface函數(shù),就不會得到IA接口,這當(dāng)然是不允許的。所以一個支持聚合的組件,它的IUnknown實現(xiàn)必然要有別于普通組件。
B組件應(yīng)該擁有一個成員變量IUnknown* m_pUnknownOuter;該指針可以指向A組件的IUnknown接口。當(dāng)客戶調(diào)用IB::QueryInterface請求時,如果IB接口已經(jīng)被聚合了,就調(diào)用m_pUnknownOuter->QueryInterface方法,這實際上就是調(diào)用了A組件的QueryInterface方法。那么,m_pUnknownOuter指針是什么時候被初始化的呢?CoCreateInstance函數(shù)和IClassFactory::CreateInstance方法都接受一個IUnknown參數(shù)。如果A組件內(nèi)部想聚合B組件的IB接口,他就會將自己的IUnknown指針傳遞進去,如果A組件并不想聚合B組件,那么簡單的傳遞一個NULL就行了。
B組件可以根據(jù)m_pUnknownOuter是否為NULL,來判斷是否被聚合。
B組件實現(xiàn)的具體步驟如下:
1) 聲明一個INondelegationUnknown接口,該接口擁有IUnknown接口一樣的純虛函數(shù),當(dāng)然函數(shù)名前面均加上Nondelegation前綴。
2) CB類(假設(shè)CB類為組件類)繼承并實現(xiàn)INondelegationUnknown接口,實現(xiàn)代碼和普通組件的IUnknown一樣,這用于非聚合的情況下。
3) CB類的構(gòu)造函數(shù)接受IUnknown指針,如果傳遞進來的是NULL,說明B組件并不被用作聚合。m_pUnknownOuter變量就指向B組件自身的IUnknown接口。如果傳遞進來的不是NULL,說明被用作聚合。m_pUnknownOuter變量值等于參數(shù)值,指向外部組件的IUnknown接口指針。
4) CB類也要繼承并實現(xiàn)IUnknown接口。這個接口的QueryInterface函數(shù)實現(xiàn)只是調(diào)用m_pUnknownOuter->QueryInterfac方法。m_pUnknownOuter究竟代表什么取決于創(chuàng)建組件時是否傳遞了外部組件的IUnknown指針。
5) B組件的類廠的CreateInstance方法內(nèi)部創(chuàng)建CB類時,將IUnknown* pUnkownOuter參數(shù)傳遞給構(gòu)造函數(shù)。創(chuàng)建成功后,調(diào)用B組件自身的(而不是外部的)NondelegationQueryInterface方法,將自身的INondelegationUnknown傳遞出去給組件A。
外部組件的實現(xiàn)
外部組件A要創(chuàng)建組件B的實例,并保存B組件的INondelegationUnknown接口指針。該接口指針是通過上一節(jié)5)由B的類廠返回的。m_pUnknownInner變量保存了B接口的INondelegationUnknown接口指針。外部組件調(diào)用CoCreateInstance函數(shù)創(chuàng)建聚合組件時,iid必須等于IID_IUnknown。
外部組件要修改自己的QueryInterface,當(dāng)客戶程序請求IB接口的時候,將調(diào)用m_pUnknownInner->QueryInterface方法。
外部組件可以通過QueryInterface的代碼來控制是否聚合B組件所有的接口。如果不想聚合B組件實現(xiàn)的IC接口??梢栽谳斎?yún)?shù)iid等于IID_IC的時候,返回E_NOINTERFACE。
聚合B組件的所有接口的方式稱為盲聚合。盲聚合的缺點是:如果B組件實現(xiàn)了IPersist接口,客戶調(diào)用IPersist::GetClassID方法時,獲得的是CLSID_CB,這就暴露了內(nèi)部的B組件;還有就是B組件實現(xiàn)的接口不了解A組件的狀態(tài),如果B組件實現(xiàn)了ISave接口,客戶調(diào)用了它,卻不能正確完成保存組件狀態(tài)的功能。所以,一般我們要避免使用盲聚合,而要有選擇的聚合。
外部組件還有一個重要的任務(wù)就是控制內(nèi)部組件的生命周期。因為內(nèi)部組件擁有外部組件的IUnknown指針,這時候當(dāng)調(diào)用內(nèi)部組件的AddRef/Release,改變的是外部組件的引用計數(shù)。所以如果需要減少引用計數(shù),應(yīng)該調(diào)用pUnknowOuter(pUnknowOuter其實就是this指針的強制轉(zhuǎn)換)->Release( )。CA類析構(gòu)時,要采用以下特殊的做法保證不會被過早的析構(gòu)和多次釋放。
m_cRef=1;
IUnknown* pUnknownOuter = this;
pUnknownOuter->AddRef( );
m_pIB->Release( );
需要注意的是CA的析構(gòu)函數(shù)是在A組件自身的引用計數(shù)為0時才會被調(diào)用,如果沒有m_cRef=1這行代碼,m_pIB->Release( )會導(dǎo)致析構(gòu)函數(shù)再次被調(diào)用。
` 最后,析溝函數(shù)將釋放組件B。通過調(diào)用B的INondelegationUnknown::Release( )方法。
代碼如下:
if(m_pUnknownInner!=NULL)
{
m_pUnknownInner->Release ( );
}
以上總結(jié)主要來自于<<COM技術(shù)內(nèi)幕>>。但是在ATL中,一切都被隱藏的很多。
使用ATL7.1創(chuàng)建ATL簡單對象(傳統(tǒng)COM對象時)向?qū)е锌梢赃x擇是否支持聚合,默認是支持的。我們現(xiàn)在開始創(chuàng)建內(nèi)部組件。
按照上圖步驟創(chuàng)建組件B,并且實現(xiàn)了IB接口。現(xiàn)在我們?yōu)镮B增加方法F( )。F()方法很簡單,只是彈出一個消息框。
STDMETHODIMP CB::F(void)
{
MessageBox(NULL,"OK","OK",MB_OK);
return S_OK;
}
在ATL中,類廠是由CComCoClass類的宏DECLARE_CLASSFACTORY()實現(xiàn)的。該宏的定義為:#define DECLARE_CLASSFACTORY() DECLARE_CLASSFACTORY_EX(ATL::CComClassFactory)
CComClassFactory實現(xiàn)了IClassFactory接口。最主要的就是實現(xiàn)了創(chuàng)建組件的函數(shù)。
// IClassFactory
STDMETHOD(CreateInstance)(LPUNKNOWN pUnkOuter, REFIID riid, void** ppvObj)
{
ATLASSERT(m_pfnCreateInstance != NULL);
HRESULT hRes = E_POINTER;
if (ppvObj != NULL)
{
*ppvObj = NULL;
// can't ask for anything other than IUnknown when aggregating
if ((pUnkOuter != NULL) && !InlineIsEqualUnknown(riid))
{
ATLTRACE(atlTraceCOM, 0, _T("CComClassFactory: asked for non IUnknown interface while creating an aggregated object"));
hRes = CLASS_E_NOAGGREGATION;
}
else
hRes = m_pfnCreateInstance(pUnkOuter, riid, ppvObj);
}
return hRes;
}
m_pfnCreateInstance是創(chuàng)建COM對象的函數(shù)的指針。創(chuàng)建CB類的函數(shù)指針是被保存到對象映射表中的用于創(chuàng)建CB類的函數(shù)指針。對象映射表是一個_ATL_OBJMAP_ENTRY結(jié)構(gòu)數(shù)組,該結(jié)構(gòu)數(shù)組還保存了創(chuàng)建類廠的函數(shù)指針。該結(jié)構(gòu)數(shù)組的初始化是通過B.h中的宏OBJECT_ENTRY_AUTO(__uuidof(B), CB)來完成的。該宏取代了ATL3中的BEGIN_OBJECT_MAP()/OBJECT_ENTRY()/END_OBJECT_MAP()宏。
宏DECLARE_CLASSFACTORY_EX定義為#define DECLARE_CLASSFACTORY_EX(cf) typedef ATL::CComCreator< ATL::CComObjectCached< cf > > _ClassFactoryCreatorClass;
CComObjectCached類派生自CComClassFactory,該類的作用是使得組件類的生命周期和服務(wù)器生命周期相同,所以適用于進程內(nèi)組件。CComCreator類只提供了一個CreateInstance函數(shù),用來幫助我們創(chuàng)建一個組件類,這里就幫助我們創(chuàng)建類廠。
所以宏DECLARE_CLASSFACTORY()的功能就是:
1) 通過從CComClassFactory類派生實現(xiàn)了IClassFactory接口
2) 使用CComObjectCached類指定了類廠對象的生命周期和服務(wù)器生命周期相同
3) 定義了_ClassFactoryCreatorClass類型,該類型的CreateInstance函數(shù)可以創(chuàng)建類廠對象,而且是采用多步構(gòu)造的方式,防止了構(gòu)造期間的意外析構(gòu)。
該宏的定義如下:
#define DECLARE_AGGREGATABLE(x) public:/
typedef ATL::CComCreator2< ATL::CComCreator< ATL::CComObject< x > >, ATL::CComCreator< ATL::CComAggObject< x > > > _CreatorClass;
_CreatorClass類型的CreateInstance函數(shù)即可以創(chuàng)建獨立組件,也可以創(chuàng)建聚合組件,取決于傳入的pOuterIUnknown參數(shù)是否為NULL。
值得一提的是CComAggObject類通過兩次從CComObjectRootEx繼承,實現(xiàn)了內(nèi)部組件的兩個IUnknown接口,一個是IUnknown*,一個是INondelegationUnknown*(其實ATL中不使用INondelegationUnknown名字,但是作用一樣和INondelegationUnknown一樣)。
如果我們在向?qū)е羞x擇不支持聚合,向?qū)谖覀兊腃B類內(nèi)加上宏#define DECLARE_NOT_AGGREGATABLE(x)
#define DECLARE_NOT_AGGREGATABLE(x) public:/
typedef ATL::CComCreator2< ATL::CComCreator< ATL::CComObject< x > >, ATL::CComFailCreator<CLASS_E_NOAGGREGATION> > _CreatorClass;
這樣_CreatorClass類型的CreateInstance函數(shù)當(dāng)接收到不為NULL的pOuterIUnknown參數(shù)時會失敗。
所以,當(dāng)我們有了ATL的支持后,我們創(chuàng)建內(nèi)部組件的一切工作只是在向?qū)е羞x擇支持聚合,輕松愜意!
現(xiàn)在我們另創(chuàng)建一個名為OuterComponent的ATL項目,創(chuàng)建A組件并實現(xiàn)IA接口。創(chuàng)建步驟就不再敘述。A組件是否支持聚合無關(guān)緊要,我們這里就默認支持吧。在OuterComponent項目中導(dǎo)入InnerComponent.dll,然后將CComPtr<IUnknown> m_pIB作為CA的成員變量。請參考下面的代碼的注釋
// A.h : CA 的聲明
#pragma once
#include "resource.h" // 主符號
#include "OuterComponent.h"
// CA
class ATL_NO_VTABLE CA :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CA, &CLSID_A>,
public IDispatchImpl<IA, &IID_IA, &LIBID_OuterComponentLib, /*wMajor =*/ 1, /*wMinor =*/ 0>
{
public:
CA()
{
}
DECLARE_REGISTRY_RESOURCEID(IDR_A)
BEGIN_COM_MAP(CA)
COM_INTERFACE_ENTRY(IA)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY_AUTOAGGREGATE(IID_IB,m_spunkIB.p,CLSID_B)//自動聚合IB接口,有了該宏就不需要在FinalConstruct函數(shù)中創(chuàng)建B對象了
END_COM_MAP()
DECLARE_GET_CONTROLLING_UNKNOWN()//創(chuàng)建虛函數(shù)IUnknown* GetControllingUnknown(),CComContainedObject類會繼承并實現(xiàn)該函數(shù),然后將返回最外層組件的IUnknown指針
DECLARE_PROTECT_FINAL_CONSTRUCT()
HRESULT FinalConstruct()
{
return S_OK;
}
void FinalRelease()
{
m_spunkIB.Release();//避免兩次析構(gòu)內(nèi)部組件
}
private:
CComPtr<IUnknown> m_spunkIB;//保存被聚合的內(nèi)部組件的IUnknown接口
};
OBJECT_ENTRY_AUTO(__uuidof(A), CA)
現(xiàn)在我們來用一下,創(chuàng)建一個客戶程序,它導(dǎo)入A和B的組件類型庫,然后創(chuàng)建A組件,然后查詢IB接口并調(diào)用IB::F()方法。
// Client.cpp : 定義控制臺應(yīng)用程序的入口點。
//
#include "stdafx.h"
#import "../../OuterComponent/OuterComponent/debug/OuterComponent.tlb" no_namespace named_guids raw_interfaces_only
#import "../../InnerComponent/InnerComponent/debug/InnerComponent.tlb" no_namespace named_guids raw_interfaces_only
int _tmain(int argc, _TCHAR* argv[])
{
::CoInitialize(NULL);
CComPtr<IA> pA;
HRESULT hr=pA.CoCreateInstance(CLSID_A);
CComPtr<IB> pB;
hr=pA->QueryInterface(&pB);
pB->F();
pB.Release();
pA.Release();
::CoUninitialize();
return 0;
}
一切成功。ATL大大簡化了我們對于聚合的使用。