Средства разработки приложений

         

Часть 2: Наследование классов и наследование интерфейсов


В первой части стать мы показали значимость неизменности интерфейсов и продемонстрировали, как разработчик может построить приложение, которое может легко заменять компоненты, если разработан интерфейс. А что, если интерфейс существующего COM-сервера имеет сотни методов? В примере из первой части мы сделали это простым клонированием интерфейса IlevelGetter, поскольку он содержал только четыре метода. Попробуйте с помощью OLE/COM Object Viewer просмотреть некоторые другие библиотеки типов на вашем компьютере. Как вы можете убедиться, многие компоненты имеют интерфейсы с весьма значительным количеством методов. Клонирование интерфейсов, которые реализуют сотни методов, с целью изменить всего лишь несколько из них было бы весьма обременительно.

Правила COM гласят, что если вы наследуете интерфейс из существующего интерфейса, вам необходимо реализовывать все методы, поскольку описания интерфейсов содержат чисто виртуальные функции. То же правило, которое обеспечивает взаимозаменяемость деталей машин, может обернуться для разработчиков тяжелым, ненужным бременем!

А что, если вы смогли бы наследовать интерфейсы без необходимости повторно описывать реализацию всех методов? Что если бы вы могли создать компонент, унаследовать интерфейсы и функциональное назначение и переделать функциональность по своему усмотрению? Сегодня это нельзя сделать с помощью COM объектов, разработанных вне вашей организации. Однако, если разработчики в вашей организации используют язык программирования, поддерживающий наследование и полиморфизм, типа Visual C++, вы это действительно сделаете. На самом деле, как мы покажем, MFC позволяет сделать это значительно легче.

В корне MFC есть CCmdTarget. CCmdTarget - это не только базовый класс для message-map архитектуры, он также содержит Dispatch планы, которые влияют на интерфейсы, такие как IDispatch и IUnknown. Каждый прямой потомок CCmdTarget, созданный с помощью Class Wizard, содержит эти интерфейсы со своими собственными CLSID. CCmdTarget - один из основных рабочих классов и базовый класс для таких "повседневных" MFC классов, как CView, CWinApp, CDocument, CWnd и CFrameWnd.
Соответственно, каждый производный класс от CCmdTarget может реализовывать собственные CLSID и интерфейсы.

Пример, который мы собираемся рассмотреть, покажет наследование интерфейсов путем образования новых C++ производных классов от CCmdTarget. В нашем базовом классе мы реализуем интерфейс с методами, которые вызывают виртуальные функции членов C++ класса. Наш производный класс заменит некоторые из отобранных виртуальных функций. Что особенно важно, вместо реализации наследуемого класса в той же DLL, мы создадим отдельную DLL со своим собственным CLSID. Наиболее эффективно наследовать реализацию интерфейса от одного кода в другой без переписывания исходного интерфейса.

Давайте начнем с просмотра кода в проекте BaseLevelGetterDLL. BaseLevelGetterDLL является типичной MFC DLL. Она была создана с помощью AppWizard как "regular DLL using the shared MFC DLL". Она также поддерживает автоматику (automation). Завершив работу с AppWizard, получаем BaseLevelGetterExport.h, а BASE_LEVEL_GETTER_DLL оказывается включенной как preprocessor definition в Project | Settings | C++. BaseLevelGetterExport.H и диалог Project | Settings приведены ниже.

//BaseLevelGetterExport.h #ifndef BASE_LEVEL_GETTER_EXPORT_DLL_H #define BASE_LEVEL_GETTER_EXPORT_DLL_H #if defined(BASE_LEVEL_GETTER_DLL) #define BASE_LEVEL_GETTER_EXPORT __declspec(dllexport) #else #define BASE_LEVEL_GETTER_EXPORT __declspec(dllimport) #endif #endif //BASE_LEVEL_GETTER_EXPORT_DLL_H



Определив BASE_LEVEL_GETTER_DLL, мы можем создавать классы и экспортировать их из нашей DLL.

Следующим шагом будет создание C++ класса, который содержит наши интерфейсы. С помощью Class Wizard несколькими нажатиями кнопки мыши мы создадим класс, наследованный от CCmdTarget. Выделив Createable by type ID в диалоге New Class, мы создадим наш новый класс с макросом IMPLEMENT_OLECREATE, присваивающем классу его собственные CLSID и интерфейс IDispatch.



Обращаясь к BaseLevelGetter.CPP, мы видим CLSID:

//Here is our CLSID // {C20EA055-F61C-11D0-A25F-000000000000} IMPLEMENT_OLECREATE(BaseLevelGetter, "BaseLevelGetterDLL.BaseLevelGetter", 0xc20ea055, 0xf61c, 0x11d0, 0xa2, 0x5f, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0) И интерфейс под названием IbaseLevelGetter типа IDispatch:



// {C20EA054-F61C-11D0-A25F-000000000000} static const IID IID_IBaseLevelGetter = { 0xc20ea054, 0xf61c, 0x11d0, { 0xa2, 0x5f, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 } }; BEGIN_INTERFACE_MAP(BaseLevelGetter, CCmdTarget) INTERFACE_PART(BaseLevelGetter, IID_IBaseLevelGetter, Dispatch) END_INTERFACE_MAP() Вместо того, чтобы работать с интерфейсом, предоставляемым по умолчанию Class Wizard, мы собираемся добавить наш собственный интерфейс, чтобы показать как легко добавлять интерфейсы в классы-потомки от CCmdTarget. Первое, что мы должны сделать, - это описать наши интерфейсы. Определение интерфейса всегда одинаково. Каждый интерфейс должен иметь IID и IUnknown как основной интерфейс где-нибудь в своей иерархии. Также необходимо реализовать три метода IUnknown. В ILevelGetter.H мы используем GUIDGEN.EXE ( находится в \Program Files\DevStudio\VC\Bin) для генерации уникального IID для нашего интерфейса наследуем интерфейс от IUnknown. Дополнительно к трем виртуальным функциям IUnknown мы добавили еще 4 виртуальные функции, которые будут реализованы в нашем COM объекте. Ниже приведен полный код ILevelGetter.H.

#ifndef ILEVELGETTER_H #define ILEVELGETTER_H // {BCB53641-F630-11d0-A25F-000000000000} static const IID IID_ILevelGetter = { 0xbcb53641, 0xf630, 0x11d0, { 0xa2, 0x5f, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 } }; interface ILevelGetter : public IUnknown { //first add the three always required methods virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID* ppvObj) = 0; virtual ULONG STDMETHODCALLTYPE AddRef() = 0; virtual ULONG STDMETHODCALLTYPE Release() = 0; //now add methods for this custom interface virtual HRESULT STDMETHODCALLTYPE GetCurrentLevel(long* plCurrentLevel) = 0; virtual HRESULT STDMETHODCALLTYPE GetHighestPossibleSafeLevel(long* plHighestSafeLevel) = 0; virtual HRESULT STDMETHODCALLTYPE GetLowestPossibleSafeLevel(long* plLowestSafeLevel) = 0; virtual HRESULT STDMETHODCALLTYPE GetTextMessage(BSTR* ppbstrMessage) = 0; }; Следующим шагом будет определение методов интерфейса в BaseLevelGetter.H.


В верхней части BaseLevelGetter.H добавим директиву include для описания нашего интерфейса как это показано ниже:

#include "ILevelGetter.h" Как только мы включили ILevelGetter.H, мы можем добавить наши методы интерфейса, используя макрос BEGIN_INTERFACE_PART. В итоге BEGIN_INTERFACE_MACRO создает вложенный класс типа XLevelGetter и член класса m_xLevelGetter в BaseLevelGetter. (Более подробное описание макроса BEGIN_INTERFACE_PART смотри MFC Technical Note 38.) Каждый метод в интерфейсе объявляется в макросе так же, как если бы никакого макроса не было. Можно убедиться, что объявления метода в ILevelGetter.H такие же как и в версии с использованием ATL.

BEGIN_INTERFACE_PART(LevelGetter, ILevelGetter) STDMETHOD(GetCurrentLevel) (long* plCurrentLevel); STDMETHOD(GetHighestPossibleSafeLevel) (long* plHighestSafeLevel); STDMETHOD(GetLowestPossibleSafeLevel) (long* plLowestSafeLevel); STDMETHOD(GetTextMessage) (BSTR* ppbstrMessage); END_INTERFACE_PART(LevelGetter) Поскольку наша цель заключается в эффективном наследовании интерфейсов из одного источника в другой без необходимости повторного описания реализации всех методов, мы собираемся добавить четыре виртуальных функции в наш класс. Каждая виртуальная функция будет соответствовать методу в интерфейсе ILevelGetter. В примере эти методы описаны в нижней части class declaration сразу после макроса BEGIN_INTERFACE_PART.

//since the class can be dynamically created //these virtual functions cannot be pure virtual long GetCurrentLevel(); virtual long GetHighestSafeLevel(); virtual long GetLowestSafeLevel(); virtual CString GetMessage(); Отметим, что поскольку наш класс-потомок от CCmdTarget использует DECLARE_DYNCREATE, эти функции не могут быть чисто виртуальными.

Последнее, что осталось сделать, - объявить наш класс "exportable". Для этого нам необходимо всего лишь включить наше описание экспорта в описание класса. Это выглядит так:

#include "BaseLevelGetterExport.h" class BASE_LEVEL_GETTER_EXPORT BaseLevelGetter : public CCmdTarget { Реализация нашего интерфейса также проста.


Первое, что нужно сделать, - это добавить поддержку нашему новому интерфейсу ILevelGetter. Общее правило заключается в добавлении макроса INTERFACE_PART между BEGIN_INTERFACE_PART и END_INTERFACE_PART для каждого поддерживаемого интерфейса. В BaseLevelGetter.CPP это делается дополнением следующей строки:

INTERFACE_PART(BaseLevelGetter, IID_ILevelGetter, LevelGetter) Так что полное описание INTERFACE_PART выглядит следующим образом:

BEGIN_INTERFACE_MAP(BaseLevelGetter, CCmdTarget) INTERFACE_PART(BaseLevelGetter, IID_IBaseLevelGetter, Dispatch) INTERFACE_PART(BaseLevelGetter, IID_ILevelGetter, LevelGetter) END_INTERFACE_MAP() Далее мы описываем реализацию методов ILevelGetter. Первые три метода, которые должны быть реализованы, - это QueryInterface, AddRef и Release из IUnknown. Эти методы показаны ниже.

//------------------------------------------------------------------------ HRESULT FAR EXPORT BaseLevelGetter::XLevelGetter::QueryInterface ( REFIID iid, LPVOID* ppvObj ) { METHOD_PROLOGUE_EX_(BaseLevelGetter, LevelGetter) return (HRESULT) pThis->ExternalQueryInterface(&iid, ppvObj); } //------------------------------------------------------------------------- ULONG FAR EXPORT BaseLevelGetter::XLevelGetter::AddRef() { METHOD_PROLOGUE_EX_(BaseLevelGetter, LevelGetter) return (ULONG) pThis->ExternalAddRef(); } //------------------------------------------------------------------------- ULONG FAR EXPORT BaseLevelGetter::XLevelGetter::Release() { METHOD_PROLOGUE_EX_(BaseLevelGetter, LevelGetter) return (ULONG) pThis->ExternalRelease(); } Четыре метода ILevelGetter реализуются весьма просто. Вместо фактического выполнения обработки, каждый метод вызывает свою связанную функцию через указатель pThis. На самом деле это требует некоторых дополнительных объяснений. Если вы посмотрите на определение макроса BEGIN_INTERFACE_PART(...) (файл ...\MFC\include\AFXDISP.H), вы обратите внимание, что этот макрос является вложенным описанием класса. Макрос делает вложенный класс (в нашем случае, XLevelGetter) производным от интерфейса (ILevelGetter в нашем примере) и объявляет его в пределах существующего класса (BaseLevelGetter).



Макрос END_INTERFACE_PART(...) завершает "внутреннее" описание класса XLevelGetter и объявляет переменную члена этого класса m_xLevelGetter. Поскольку m_xLevelGetter является членом класса BaseLevelGetter, мы могли бы некоторыми сложными арифметическими операциями над указателями передать от this объекта XLevelGetter в this объекта, содержащего BaseLevelGetter. Однако библиотека MFC содержит другой макрос, выполняющий то же самое. Он называется METHOD_PROLOGUE_EX_, и в нашем конкретном случае он создаст переменную BaseLevelGetter* pThis. Вы можете использовать pThis для доступа к public членам и методам "внешнего" класса BaseLevelGetter, включая виртуальные (полиморфные) функции. Вызов виртуальных функций во "внешнем" классе, фактически, приводит к наследованию интерфейса. Обратите внимание, что виртуальные функции BaseLevelGetter возвращают бессмысленные значения и содержат комментарии, чтобы позволить разработчикам, создающим производные классы, переписать эти функции.

Другой способ показать виртуальное отношение, возможно значительно более удобный для чтения, - это "указать владельца объекта" (set an owner object) в классе XLevelGetter (класс, созданный макросом BEGIN_INTERFACE_PART). Внутри макроса BEGIN_INTERFACE_PART (BaseLevelGetter.H) мы добавляем две функции, и член класса выглядит следующим образом:

XLevelGetter() { m_pOwner = NULL; } //constructor sets member to NULL void SetOwner( BaseLevelGetter* pOwner ) { m_pOwner = pOwner; } //set the member BaseLevelGetter* m_pOwner; //class member Внутри конструктора BaseLevelGetter мы вызываем XLevelGetter::SetOwner. Как упоминалось выше, макрос BEGIN_INTERFACE_PART добавляет в BaseLevelGetter член класса m_xLevelGetter, который представляет LevelGetter. В конструкторе BaseLevelGetter мы вызываем:

m_xLevelGetter.SetOwner( this ); который присваивает m_pOnwer значение действительного объекта.

Ниже показана реализация четырех методов ILevelGetter и четырех ассоциированных виртуальных функций BaseLevelGetter.


Остальные два метода (GetLowestPossibleSafeLevel и GetTextMessage) реализованы по принципу использования "владельца объекта".

//------------------------------------------------------------------------ STDMETHODIMP BaseLevelGetter::XLevelGetter::GetCurrentLevel ( long* plCurrentLevel ) { METHOD_PROLOGUE_EX_(BaseLevelGetter, LevelGetter) //call outer object's GetCurrentLevel //whether this class or a derived class *plCurrentLevel = pThis->GetCurrentLevel(); return S_OK; } //------------------------------------------------------------------------- STDMETHODIMP BaseLevelGetter::XLevelGetter::GetHighestPossibleSafeLevel ( long* plHighestSafeLevel ) { METHOD_PROLOGUE_EX_(BaseLevelGetter, LevelGetter) //call outer object's GetHighestSafeLevel //whether this class or a derived class *plHighestSafeLevel = pThis->GetHighestSafeLevel(); return S_OK; } //------------------------------------------------------------------------- STDMETHODIMP BaseLevelGetter::XLevelGetter::GetLowestPossibleSafeLevel ( long* plLowestSafeLevel ) { METHOD_PROLOGUE_EX_(BaseLevelGetter, LevelGetter) //call outer object's GetLowestSafeLevel //whether this class or a derived class if( m_pOnwer != NULL) { *plLowestSafeLevel = m_pOwner->GetHighestSafeLevel(); } else { ASSERT(FALSE); } return S_OK; } //------------------------------------------------------------------------ STDMETHODIMP BaseLevelGetter::XLevelGetter::GetTextMessage ( BSTR* ppbstrMessage ) { METHOD_PROLOGUE_EX_(BaseLevelGetter, LevelGetter) //call outer object's GetMessage //whether this class or a derived class CString sMessage; If( m_pOwner != NULL ) { sMessage = m_pOwner->GetMessage(); } else { ASSERT(FALSE); } *ppbstrMessage = sMessage.AllocSysString(); return S_OK; } //--------------------------------------------------------------------- long BaseLevelGetter::GetCurrentLevel() { TRACE("Derived classes should override!"); return -1; } //--------------------------------------------------------------------- long BaseLevelGetter::GetHighestSafeLevel() { TRACE("Derived classes should override!"); return -1; } //--------------------------------------------------------------------- long BaseLevelGetter::GetLowestSafeLevel() { TRACE("Derived classes should override!"); return -1; } //--------------------------------------------------------------------- CString BaseLevelGetter::GetMessage() { TRACE("Derived classes should override!"); return "BaseLevelGetter"; } Скомпилируйте и слинкуйте приложение.


Как только DLL создана, скопируйте ее в каталог Windows\System (\WINNT\System32 для Windows NT).

Важно: Поскольку мы будем использовать интерфейс ILevelGetter из BaseLevelGetter, не забудьте после помещения этой DLL в соответствующий каталог зарегистрировать ее с помощью RegSvr32. Если бы мы использовали BaseLevelGetter как абстрактный базовый класс (т.е. виртуальные функции BaseLevelGetter должны были бы быть переопределены) и при этом, возможно, удалось бы избежать ошибок в реализации, тогда не было бы необходимости регистрировать COM объект с помощью RegSvr32. Чтобы построить COM объект, который реализует интерфейс ILevelGetter, но не требует переопределения всех методов, мы создаем COM DLL точно так же, как BaseLevelGetterDLL: мы создаем MFC AppWizard DLL, которая поддерживает automation, и добавляем класс, являющийся потомком CCmdTarget. Пример содержит проект HotTubLevelGetterDLL с классом HotTubLevelGetter - потомком от CmdTarget, который создается через диалог New Class в Class Wizard, как показано ниже.



Далее добавляем BaseLevelGetterDLL в путь include, указав его как каталог Additional Include на закладке Project | Settings | C/C++ , как показано ниже.



И линкуем BaseLevelGetterDLL.lib, добавляя ее как Library Module на закладке Project | Settings | Link.



Завершив все установки проекта, выполним следующие пять шагов для полного завершения создания COM DLL plug-in.

1. Открыть HotTubLevelGetter.H и заменить все instances из CCmdTarget на BaseLevelGetter (существует единственная instance CCmdTarget в HotTubLevelGetter.H).



2. Добавить BaseLevelGetter.H как include:

#include <BaseLevelGetter.h> class HotTubLevelGetter : public BaseLevelGetter { 3. Переписать виртуальные функции BaseLevelGetter как это требуется. В примере объявляются две следующие виртуальные функции:

virtual CString GetMessage( ) { return "HotTubLevelGetter"; } virtual long GetCurrentLevel( ) { return -2; } 4. Открыть HotTubLevelGetter.CPP и заменить все instances из CCmdTarget на BaseLevelGetter (существует пять instances CCmdTarget в HotTubLevelGetter.CPP).



5. Выполнить компиляцию и линковку. Не забудьте зарегистрировать вашу COM DLL через RegSvr32.

Прежде чем продемонстрировать работу COM plug-in на клиенте, давайте посмотрим что мы построили. Классы BaseLevelGetter и HotTubLevelGetter оба являются потомками CCmdTarget. Когда мы создавали HotTubLevelGetter, мы указали Class Wizard наследовать его от CCmdTarget. Напомним, что каждый класс, созданный Class Wizard как прямой потомок CCmdTarget, поддерживает собственные CLSID и интерфейс IDispatch. Когда мы изменяем базовый класс HotTubLevelGetter с CCmdTarget на BaseLevelGetter, HotTubLevelGetter наследует виртуальные методы BaseLevelGetter.

Когда клиенту необходим доступ к HotTubLevelGetter, он выполняет обычный CoCreateInstance(...) - передавая CLSID HotTubLevelGetter и IID_ILevelGetter, и вызывая методы ILevelGetter. Когда выполняется какой-либо метод, например, GetCurrentLevel, METHOD_PROLOGUE_EX_ берет значение pThis из offset table, и pThis действительно указывает на instance HotTubLevelGetter. То же вещь происходит, когда мы используем m_pOwner (он также указывает на instance HotTubLevelGetter); это немного легче для понимания из-за того, что мы можем наблюдать как выполняется метод m_xLevelGetter.SetOwner( this ). Давайте посмотрим на клиентское приложение и установим некоторые точки прерывания.

Откройте LevelViewer в папке LevelViewer2. Этот проект почти идентичен первому варианту LevelViewer. OnFish позиционирован в BaseLevelGetter, а OnGas - в HotTubLevelGetter, как показано далее.

//----------------------------------------------------------- void CLevelViewerDlg::OnFish() //mapped to BaseLevelGetter { m_sLastCalled = _T("CheckedFish"); CLSID clsid; HRESULT hRes = AfxGetClassIDFromString("BaseLevelGetterDLL.BaseLevelGetter", &clsid); if(SUCCEEDED(hRes)) SetNewData(clsid, IID_ILevelGetter); } //------------------------------------------------------------ void CLevelViewerDlg::OnGas() //mapped to HotTubLevelGetter { m_sLastCalled = _T("CheckedGas"); CLSID clsid; HRESULT hRes = AfxGetClassIDFromString("HotTubLevelGetterDLL.HotTubLevelGetter", &clsid); if(SUCCEEDED(hRes)) SetNewData(clsid, IID_ILevelGetter); } Обе функции вызывают SetNewData, передавая CLSID, созданный Class Wizard, и IID_ILevelGetter, описанный в ILevelGetter.H и включенный вLevelViewerDlg.H.



Замечание: на закладке C++ , категория Preprocessor, добавьте ..\BaseLevelGetterDLL в качестве дополнительного include каталога.

SetNewData работает точно также, как и раньше. Постройте и слинкуйте приложение, но перед запуском поставьте точки прерывания на каком-нибудь одном или на всех методах интерфейса как показано ниже.



Когда выполнение остановится на точке прерывания, воспользуйтесь режимом "Step Into" (по клавише F8 или F11 в зависимости от установленной системы) и пошаговым проходом (F10) дойдите до строки с указателем объекта pThis или m_pOwner. Проверьте значение. В зависимости от того, подключил таймер HotTubLevelGetter или BaseLevelGetter, pThis (или m_pOnwer) будут указывать на правильный объект.





Как вы видели, COM plug-ins - весьма мощный метод разработки приложений, который может использоваться в реальных ситуациях, например, такой как описана ниже.

Медицинская страховая компания, которая занимается разработкой заказных планов страхования для крупных компаний, приходит к вам за советом относительно разработки новой Windows системы. Каждый раз, когда они добавляют новый план, они должны разработать новую логику обработки или внести небольшое исправление в обрабатывающую логику для существующего плана, но не должны пересматривать каждую часть функционального назначения. Но поскольку они все время добавляют планы в свою систему, модификация, переопределение реализации или клонирование апробированной исходной программы непрактично, поскольку это чревато потерей целостности работающего кода, независимо от аккуратности программистов.

Как разработчик COM на C++, вы понимаете потребность в заменяемых компонентах, которые поддерживают полиморфизм. Рассмотрим следующее: интерфейс IBasePlan, входящий в класс BasePlan, реализует 100 методов интерфейса. Требования плана ABC включают модификацию реализации 50 методов в интерфейсе IBasePlan. Требования плана XYZ включают модификацию реализации 51 метода в интерфейсе IBasePlan, но 50 из них точно такие же, как для плана ABC.


Вместо полного определения реализации для каждого COM объекта, вы назначаете в BasePlan 100 виртуальных функций члена C++ класса, по одной для каждого метода в интерфейсе IBasePlan , как вышеприведенном примере.

Поскольку у вас есть ассоциированные виртуальные функции в классе BasePlan, иерархия класса для плана XYZ такова:

1. class BASE_PLAN_EXPORT BasePlan : public CCmdTarget

Реализует IBasePlan, 100 методов интерфейса и 100 ассоциированных виртуальных функций члена C++ класса.

2. class ABC_PLAN_EXPORT ABCPlan : public BasePlan

Наследуется от BasePlan, использует 50 виртуальных функций члена C++ класса в BasePlan и замещает 50 виртуальных функций BasePlan.

3. class XYZPlan : public ABCPlan

Наследуется от ABCPlan, использует 49 виртуальных функций члена C++ класса в BasePlan, использует 50 виртуальных функций члена C++ класса в ABCPlan и замещает 1 виртуальную функцию BasePlan.

Каждый компонент создается как отдельный binary и COM объект. Каждый из них имеет отдельный CLSID и, благодаря структуре наследования, реализует интерфейс IBasePlan. Применяя AppWizard и Class Wizard, вы можете завершить реализацию плана XYZ в течение нескольких минут без какого-либо затрагивания COM компонентов базового класса. Все библиотеки COM DLL размещаются на том же компьютере, и если вы используете компонентные категории или другие аналогичные методы регистрации, приложение клиента найдет план XYZ, как только он будет зарегистрирован RegSvr32.


Содержание раздела