COMponents

Написано и разработано Стивом Робинсоном (Steve Robinson) и Алексом Красселом (Alex Krassel), Panther Software

Обзор

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

В производстве такое ограничение недопустимо. Можете ли вы себе представить, что вам пришлось бы переделывать автомобильный двигатель, если бы вы захотели заменить ваши шины от изготовителя на более совершенные? Это могло бы пролиться золотым дождем на механиков, но чрезмерные эксплуатационные издержки приведут к уменьшению спроса на автомобили, а от этого пострадают все: потребители, производители автомобилей, те же механики. Фактически, одним из основных факторов успеха промышленной революции стала способность взаимозаменяемости деталей машин, т.е. использование компонентов. Сегодня мы не задумываясь заменяем компоненты и добавляем новые принадлежности в наши автомобили.

Автомобили ничего "не знают" о шинах, которые они используют. Шины имеют свойства (ширина колеса и пр.). Если свойства у различных шин совпадают, то эти шины взаимозаменяемы. Светильник ничего "не знает" о лампах, которые в нем используются. Если параметры ламп (диаметр завинчивающейся части) удовлетворяют требованиям изготовителя осветительного прибора, то эти лампы взаимозаменяемы. Давно ли индустрия программного обеспечения стала догонять остальную часть мира и строить компоненты, которые понятия не имеют о том, как они будут использоваться ? Для отрасли, которая считается передовой, мы действительно плетемся в хвосте.

На первый взгляд, динамически подключаемые библиотеки (DLL) обеспечивают решение указанных выше проблем. Следующая выдуманная история покажет, что это не так.

Предположим, вам нужно разработать приложение для компании Acme Gas Tanks. Приложение будет показывать уровень бензина в новом престижном топливном баке Acme на 1000 галлонов. Во-первых, вы создаете индикатор уровня на основе ActiveX(tm), который имеет три отметки: текущий уровень топлива в баке, минимально возможный безопасный уровень и максимально возможный безопасный уровень. Вы пишете DLL, назвав ее GasTankLevelGetterDLL, которая имеет следующие функции:

Естественно, GasTankLevelGetterDLL поддерживает возможность некоторого устройства непрерывно считывать данные о количестве топлива в новом топливном баке Acme. Ваше приложение работает превосходно и не "глючит".

Пару недель спустя, мистер Ричи Рич ( Richy Rich ) вызывает вас к себе и сообщает, что ваш ActiveX для индикации уровня является самой красивой вещью, которую он когда-либо видeл в своей жизни. Ричи говорит вам, что хочет использовать его для контроля уровня в своем аквариуме на 5000 галлонов. Он заявляет, что индикатор должен показывать те же три уровня, что и для топливного бака. Вы говорите ему, что зайдете к нему завтра, а пока подумаете над его предложением.

На следующий день вы приходите к мысли, называть все DLL, которые реализуют те самые три функции, хотя и с различной внутренней обработкой, одинаково - LevelGetterDLL. Проблема контроля уровня воды в аквариуме мистера Ричи решена. Он проверяет ваше приложение 24 часа в сутки, чтобы убедиться, что его рыбки находятся в полной безопасности. Вы также передаете новую версию LevelGetterDLL Acme. Другие компании связываются с вами на предмет использования вашего ActiveX индикатора уровня. Вы отвечаете им: "Нет проблем! Возьмите эти три функции, назовите вашу DLL LevelGetterDLL, и все готово." Вам необходимо всего лишь один раз перекомпилировать ваше приложение, чтобы оно поддерживало новую версию LevelGetterDLL, но поскольку во всем мире все называют свои DLL одинаково (LevelGetterDLL) и используют одинаковые неизменные три метода, то все работает превосходно, и вам никогда не придется перекомпилировать ваше приложение снова. Вы возвращаетесь домой, чувствуя себя немножко гением.

На следующий день, открыв The Wall Street Journal , вы обнаруживаете, что Ричи Рич разбился на своем вертолете. По дороге в штаб-квартиру Rich Inc. ему не хватило топлива. Похоже, Ричи был клиентом Acme и запускал оба приложения на своем компьютере одновременно. Приложение 1 было то самое, которое вы разработали с использованием LevelGetterDLL для контроля уровня в его аквариуме. Приложение 2 было сделано по заказу Acme для контроля уровня топлива, в нем использовалась та же версия LevelGetterDLL, которая была установлена на вертолете Ричи. И хотя Ричи запускал оба приложения, Приложение 2 для топливных баков Acme использовало DLL LevelGetterDLL для аквариума и показывало уровни 5000-галлонного аквариума вместо 1000-галлонного топливного бака, поскольку версия для аквариума была установлена на компьютер последней. Ричи ничего не знал о том, что его вертолету не хватит топлива. Rich Inc. подает в суд на Acme, которая, в свою очередь, подает в суд на вас. Другие компании, которым вы посоветовали ваше решение, также подают на вас в суд. Если бы вы использовали Component Object Model (COM), Ричи Рич был бы жив, и вам не пришлось бы садиться на скамью подсудимых.

Правило Если две или более DLL предоставляют одинаковые функции (immutability), вы можете использовать любую из этих DLL. Однако одно приложение не может использовать сразу несколько DLL, как и не могут одновременно несколько таких DLL находиться на одном и том же компьютере. Технология COM решает эту проблему. Два сервера COM с идентичными интерфейсами (и следовательно методами) могут использоваться двумя различными приложениями и могут находиться на одном и том же компьютере, поскольку они имеют различные идентификаторы CLSID, и, следовательно, различны на бинарном уровне. Кроме того, технически эти два сервера COM взаимозаменяемы.

Отсутствие "взаимозаменяемых деталей" (компонентов) присуще индустрии программных разработок в силу ее относительно молодого возраста. Однако, подобно индустриальной революции, создавшей независимые детали машин, технология COM реализует это через программные компоненты. Понимая смысл CLSID и неизменности интерфейсов, можно написать законченный plug-in без какого-либо знания о клиенте. Это означает, что Приложение 1 может использовать или Plug-In1 или Plug-In2. Еще лучше, чтобы Приложение 1 могло динамически переключать Plug-In1 и Plug-In2. Проектирование приложений, использующих динамически заменяемые вставки (plug-ins) сделает для программной индустрии то же самое, что сделали детали машин и механизмов для промышленной революции.

Восторгаясь Active Template Library (ATL) и Distributed COM (DCOM), мы постепенно забываем, что лежало в основе появления COM. Способность DCOM использовать удаленный вызов процедур (remote procedure calls, RPC) выстраивать данные (marshaling) воодушевляет ( и, возможно, является одной из причин роста популярности COM за последние 12 месяцев), однако это не главное, почему была разработана технология COM. Главное, ради чего создавалась COM, - предоставить производителям программ возможность встраивать новые функциональные части в существующие приложения без перестраивания этих приложений. Компоненты COM должны быть спроектированы как взаимозаменяемые вставки (plug-ins), независимо от того, является ли компонент COM локально подключаемой DLL или удаленно запускаемым сервером.

Цель

Эта статья продемонстрирует компоненты COM, которые допускают многократное использование аналогично автомобильным шинам. Использование COM позволит разработать серию программных продуктов за более короткий срок, чем без применения этой технологии. Зная как создавать COM объекты и интерфейсы, можно разрабатывать взаимозаменяемые компоненты.

Что потребуется

Для построения примеров вам потребуется Microsoft Visual C++(r) 5.0. Нет необходимости в десятилетнем опыте по Windows(r) и C, достаточно некоторого знакомства с Visual C++, MFC, наследованием и полиморфизмом. Примеры будут построены и выполнены под Windows NT(r) или Windows 95. Мы будем использовать OLE/COM Object Viewer - удобную утилиту, поставляемую вместе с Visual C++ 5.0 и Visual Basic(r) 5.0, а также доступную для download на http://www.microsoft.com/oledev.

Часть 1: Дублирование интерфейсов

В вышеприведенном случае с Ричи Ричем мы видели, что DLL для аквариума и DLL для топливного бака не могли находиться на одном и том же компьютере, потому что ни клиентское приложение, ни две DLL не были COM компонентами. Какая бы DLL ни была скопирована на компьютер, только скопированная последней будет использоваться клиентским приложением. Как мы уже видели, использование некорректной DLL может привести к катастрофическим результатам: вертолет разбился. Мы предположили, что если бы разработчик программы использовал технологию COM, то он имел бы обе DLL на машине. Поскольку две DLL были бы различимы по их CLSID, они могли бы использоваться в пределах одного приложения. По технологии COM обе DLL должны задействовать идентичные методы через заменяемые интерфейсы.

Чтобы это доказать, мы собираемся создать единственное GUI-приложение, которое использует и показывает информацию, получаемую от двух серверов COM: GasTankLevelGetter DLL и FishTankLevelGetter DLL. Мы также создадим одно приложение, которое будет получать информацию от каждой COM DLL и отображать их. Опрос каждой DLL будет происходить попеременно по четырехсекундному таймеру. Чтобы подчеркнуть неизменность интерфейсов и что COM является двоичным стандартом, мы собираемся написать GUI-приложение FishTankLevelGetter COM DLL исключительно на основе информации о GasTankLevelGetter COM DLL. Однако мы не собираемся предоставлять вам исходный код GasTankLevelGetter COM DLL. Если вы переписали пример, вы найдете GasTankLevelGetter COM DLL в папке Binaries. Мы вам даже не скажем на чем написана GasTankLevelGetter: на Delphi, Visual C++, Java(tm), Cobol, Turbo Pascal или Visual Basic. Вам, однако, придется зарегистрировать GasTankLevelGetter DLL с помощью RegSvr32.

Как только вы зарегистрировали GasTankLevelGetter DLL с помощью RegSvr32, вы готовы начать, вооружившись OLE/COM Object Viewer. Если вы используете Visual C++ 5.0, OLE/COM Object Viewer находится в программной группе Visual C++ 5.0 при навигации через Start | Programs в Explorer. Если у вас нет OLE/COM Object Viewer, спишите его из http://www.microsoft.com/oledev/ и запустите приложение.

Запустив OLE/COM Object Viewer, выберите режим View | Expert для просмотра Type Libraries. Пролистайте список и откройте папку под названием Type Libraries. Пролистайте папку пока не найдете GasTankLevelGetter 1.0 TypeLibrary (Ver 1.0). Выделите этот элемент списка и вы увидите на правой панели ID библиотеки типа и ее полный путь, как показано на рисунке.

Двойной щелчок на GasTankLevelGetter откроет окно, отображающее всю библиотеку типа. Эта информация берется их данных регистров, которые создаются при регистрации DLL. Данные по TypeLib хранятся в HKEY_CLASSES_ROOT \ TypeLib.

Раздел coclass содержит список поддерживаемых интерфейсов для компонентного объекта. Объект может иметь любое количество интерфейсов, перечисляемых в его теле и полностью описывающих тот набор интерфейсов, которые этот объект реализует, как входящих, так и исходящих. Ниже приведены CLSID и интерфейс, содержащиеся в coclass для данного COM объекта:

CLSID: 8A544DC6-F531-11D0-A980-0020182A7050
Interface Name: ILevelGetter

[
      uuid(8A544DC6-F531-11D0-A980-0020182A7050),
      helpstring("LevelGetter Class")
]
      coclass LevelGetter {
        [default] interface ILevelGetter;
      };

Раскрывая далее информацию по интерфейсу наподобие coclass, мы можем определить:

[
      odl,
      uuid(8A544DC5-F531-11D0-A980-0020182A7050),
      helpstring("ILevelGetter Interface")
]
    interface ILevelGetter : IUnknown {
    HRESULT _stdcall GetLowestPossibleSafeLevel([out, retval] long*
                      plLowestSafeLevel);
    HRESULT _stdcall GetHighestPossibleSafeLevel([out, retval]
                      long* plHighestSafeLevel);
    HRESULT _stdcall GetCurrentLevel([out, retval] long* 
                      plCurrentLevel);
    HRESULT _stdcall GetTextMessage([out, retval] BSTR* 
                      ppbstrMessage);
    };

Более детальный взгляд на структуру type library открывает нам методы и ID интерфейсов.

Теперь, поскольку мы знаем, как построить интерфейс ILevelGetter, давайте создадим наш собственный компонент COM на основе этой информации. Если вы решили работать с существующим примером, все источники находятся в папке LevelViewer. Запустите Visual C++ 5.0 и создайте новый проект. Определите тип ATLComAppWizard как проект и "FishTankLevelGetter" как имя проекта. Мы полагаем, что вы создали новую папку проекта. Окно New Project Dialog должно выглядеть как это показано ниже.

В AppWizard для Server Type укажите Dynamic Link Library (DLL). Отметьте обе опции Allow merging of proxy/stub code и Support MFC.

Когда вы создали новый проект FishTankLevelGetter, выберите в меню Insert | New Class... для создания нового ATL класса. Вы можете выбрать любое имя класса, но убедитесь, что интерфейс называется IlevelGetter, а его тип - Custom, что указывает на наследование ILevelGetter от IUnknown. Если бы ILevelGetter в GasTankLevelGetter COM DLL наследовалась от IDispatch, нам пришлось бы выбрать тип интерфейса Dual, который указывал бы на то, что новый интерфейс будет производным от IDispatch. Если диалог New Class выглядит как показано ниже, нажмите OK, чтобы создать новый класс.

Следующий шаг заключается в редактировании FishTankLevelGetter.IDL. В IDL файле вам нужно иметь новый интерфейс ILevelGetter, наследуемый из IUnknown. Если вы работаете с примерами, вы увидите следующий код, который содержит четыре одинаковых неизменяемых метода интерфейса IlevelGetter, которые мы видели в интерфейсе ILevelGetter GasTankLevelGetter.

  • [
       object,
       uuid(7F0DFAA2-F56D-11D0-A980-0020182A7050),
       helpstring("ILevelGetter Interface"),
       pointer_default(unique)
    ]
    interface ILevelGetter : IUnknown
    {
       HRESULT GetLowestPossibleSafeLevel([out, retval] long* plLowestSafeLevel);
       HRESULT GetHighestPossibleSafeLevel([out, retval] long* plHighestSafeLevel);
       HRESULT GetCurrentLevel([out, retval] long* plCurrentLevel);
       HRESULT GetTextMessage([out, retval] BSTR* ppbstrMessage);
    };
  • Если вы пишите код, как и мы, самостоятельно, вы захотите добавить вышеуказанный код так, что ваш интерфейс соответствовал четырем идентичным неизменным методам. Наиболее просто добавить код с помощью "copy and paste" непосредственно из окна ITypeLib Viewer. Ваш код должен выглядеть точно также, как в примере, за исключением ID интерфейса.

    Откройте LevelGetter.H и объявите методы в классе. В вашем классе объявление методов должно выглядеть как это показано ниже:

  • class LevelGetter : 
       public ILevelGetter,
       public CComObjectRoot,
       public CComCoClass<LevelGetter,&CLSID_LevelGetter>
    {
    public:
       LevelGetter(){}
    
    BEGIN_COM_MAP(LevelGetter)
       COM_INTERFACE_ENTRY(ILevelGetter)
    END_COM_MAP()
    //DECLARE_NOT_AGGREGATABLE(LevelGetter) 
    // Remove the comment from the line above if you don't want your object to 
    // support aggregation. 
    
    DECLARE_REGISTRY_RESOURCEID(IDR_LevelGetter)
    
    // ILevelGetter
    public:                                //THE FOUR NEW METHODS
       STDMETHOD (GetLowestPossibleSafeLevel)  (long* plLowestSafeLevel);
       STDMETHOD (GetHighestPossibleSafeLevel) (long* plHighestSafeLevel);
       STDMETHOD (GetCurrentLevel) (long* plCurrentLevel);
       STDMETHOD (GetTextMessage) (BSTR* ppbstrMessage);
    };
  • Вам теперь нужно сделать четыре метода. Для демонстрационных целей, давайте оставим методы простыми. Реализуйте их по вашему усмотрению или скопируете следующий код из образцов.

  • //---------------------------------------------------------------------------
    STDMETHODIMP LevelGetter::GetLowestPossibleSafeLevel(long* plLowestSafeLevel)
    {
       *plLowestSafeLevel = 70;
       return S_OK;
    }
    
    //---------------------------------------------------------------------------
    STDMETHODIMP LevelGetter::GetHighestPossibleSafeLevel(long* plHighestSafeLevel)
    {
       *plHighestSafeLevel = 98;
       return S_OK;
    }
    
    //---------------------------------------------------------------------------
    STDMETHODIMP LevelGetter::GetCurrentLevel(long* plCurrentLevel)
    {
       *plCurrentLevel = 94;
       return S_OK;
    }
    
    //---------------------------------------------------------------------------
    STDMETHODIMP LevelGetter::GetTextMessage(BSTR* ppbstrMessage)
    {
       *ppbstrMessage = ::SysAllocString(L"All clear, water level is fine");
       return S_OK;
    }
  • Поскольку у вас уже есть методы, скомпилируйте и слинкуйте вашу COM DLL. Затем мы начнем создавать клиентское приложение.

    Создание клиентского приложения для обоих COM объектов

    Мы собираемся создать клиентское приложение, которое будет поддерживать два COM объекта GasTankLevelGetter и FishTankLevelGetter. Используя AppWizard, создайте MFC диалог приложения, который бы поддерживал и управляющие элементы Automation, и ActiveX одновременно (укажите это в соответствующих check box во время работы с AppWizard).

    Как только вы создали приложение, отредактируйте ваш основной диалог в редакторе ресурсов, так чтобы он имел сходство с следующим:

  • Замечание: Возможно вы захотите просмотреть ID элементов управления в примере, поскольку мы собираемся изменить эти значения.
  • Следующий шаг состоит в добавлении указателей сообщений для двух кнопок Gas Tank Level и Fish Tank Level. В примере эти методы называются OnGas и OnFish соответственно

    Если вы создали класс диалога и добавили указатели сообщений для кнопок, вам необходимо открыть этот класс и добавить несколько членов класса и методов класса. Первое, что мы сделаем, - это опишем далее интерфейс ILevelGetter так, чтобы мы могли добавлять члены класса (class member) для этого типа интерфейса. Во-вторых, добавим два дополнительных метода класса (class methods) ClearMembers и SetNewData и два члена класса m_pILevelGetter и m_sLastCalled. Затем, используя Class Wizard, добавим методы OnDestroy и OnTimer. Как только это сделано, ваше описание класса должно быть таким, как показано ниже.

  • //forward declaration so for our class member
    interface ILevelGetter;
    
    class CLevelViewerDlg : public CDialog
    {
       DECLARE_DYNAMIC(CLevelViewerDlg);
       friend class CLevelViewerDlgAutoProxy;
    
    public:
       CLevelViewerDlg(CWnd* pParent = NULL);   // standard constructor
       virtual ~CLevelViewerDlg();
    
       //{{AFX_DATA(CLevelViewerDlg)
       enum { IDD = IDD_LEVELVIEWER_DIALOG };
       //}}AFX_DATA
    
       //{{AFX_VIRTUAL(CLevelViewerDlg)
       protected:
       virtual void DoDataExchange(CDataExchange* pDX);	// DDX/DDV support
       //}}AFX_VIRTUAL
    
    // Implementation
    protected:
       CLevelViewerDlgAutoProxy* m_pAutoProxy;
       HICON m_hIcon;
    
       BOOL CanExit();
    
    //added by manually typing these into the class
       void ClearMembers();
       void SetNewData(const CLSID& clsid, const IID& iid);
    
       ILevelGetter* m_pILevelGetter;
       CString m_sLastCalled;
    
       // Generated message map functions
       //{{AFX_MSG(CLevelViewerDlg)
       virtual BOOL OnInitDialog();
       afx_msg void OnPaint();
       afx_msg HCURSOR OnQueryDragIcon();
       afx_msg void OnClose();
       virtual void OnOK();
       virtual void OnCancel();
    
    //added by the Class Wizard
       afx_msg void OnFish();
       afx_msg void OnGas();
       afx_msg void OnDestroy();
       afx_msg void OnTimer(UINT nIDEvent);
       //}}AFX_MSG
       DECLARE_MESSAGE_MAP()
    };
  • Далее изменим файл описания реализации (implementation file). В конструкторе класса проинициализируйте переменные членов класса как это показано ниже:

    //--------------------------------------------------------------
    CLevelViewerDlg::CLevelViewerDlg(CWnd* pParent /*=NULL*/)
       : CDialog(CLevelViewerDlg::IDD, pParent)
    {
       //{{AFX_DATA_INIT(CLevelViewerDlg)
       //}}AFX_DATA_INIT
       m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
       m_pAutoProxy = NULL;
    
       m_pILevelGetter = NULL;
       m_sLastCalled = _T("CheckedGas");
    }

    Реализация метода ClearMembers приводится далее. Эта функция очищает элементы управления диалога (dialog controls). (Отметим, что мы использовали бы Dialog Data exchange для членов класса.)

    //--------------------------------------------------------------------
    void CLevelViewerDlg::ClearMembers()
    {
       CWnd* pWnd = GetDlgItem(IDC_TANK_TYPE);
       if(pWnd != NULL)
          pWnd->SetWindowText("");
    
       pWnd = GetDlgItem(IDC_LOWEST_SAFE);
       if(pWnd != NULL)
          pWnd->SetWindowText("");
    
       pWnd = GetDlgItem(IDC_HIGHEST_SAFE);
       if(pWnd != NULL)
          pWnd->SetWindowText("");
    
       pWnd = GetDlgItem(IDC_CURRENT);
       if(pWnd != NULL)
          pWnd->SetWindowText("");
    
       pWnd = GetDlgItem(IDC_MESSAGE);
       if(pWnd != NULL)
          pWnd->SetWindowText("");
    }

    OnDestroy, показанный ниже, используется для очистки при закрытии диалога.

    //--------------------------------------------------------------------
    void CLevelViewerDlg::OnDestroy() 
    {
       CDialog::OnDestroy();
       KillTimer(1);
    }

    Данный класс использует OnTimer для вызова методов кнопок OnFish и OnGas так, что пользователю не требуется нажимать кнопки для обновления данных.

    //--------------------------------------------------------------------
    void CLevelViewerDlg::OnTimer(UINT nIDEvent) 
    {
       if(m_sLastCalled == _T("CheckedFish"))
          OnGas();
       else
          OnFish();
    }
  • Замечание: В реальной жизни предпочтительнее использовать технологию с нажатием кнопок и интерфейс IConnectionPoint. Законченный пример такой реализации вы найдете на http://www.microsoft.com/workshop/prog/com/overview-f.htm.
  • Виртуальная функция OnInitDialog используется в основном для запуска таймера, хотя она также возвращает данные из GasTankLevelGetter COM DLL.

    //--------------------------------------------------------------------
    BOOL CLevelViewerDlg::OnInitDialog()
    {
       CDialog::OnInitDialog();
    
       SetIcon(m_hIcon, TRUE);                // Set big icon
       SetIcon(m_hIcon, FALSE);              // Set small icon
    
       OnGas();                                           //obtain data
       SetTimer(1, 4000, NULL);                //set timer for 4 seconds
    
       return TRUE;  // return TRUE  unless you set the focus to a control
    }

    Теперь мы готовы описать реализацию наших методов кнопок OnFish и OnGas, которые вызываются попеременно каждые 4 секунды. Обе эти функции идентичны на процедурном уровне; они передают CLSID и IID в SetNewData. Единственная разница состоит в том, что CLSID и IID, передаваемые методом OnGas, используются в GasTankLevelGetter, а CLSID и IID передаваемые методом OnFish, - в FishTankLevelGetter.

    OnGas возвращает CLSID, взятый из строки GUID, которая имеется в данных coclass TypeLib. Таким же образом возвращается IID и, кроме того, он отображается в OLE/COM Object Viewer. Как только получены GUID, вызывается SetNewData.

    //--------------------------------------------------------------------
    void CLevelViewerDlg::OnGas() 
    {
          m_sLastCalled = _T("CheckedGas");
       CLSID clsid;
       IID   iid;   
       HRESULT hRes;
          hRes = AfxGetClassIDFromString(
                                 "{8A544DC6-F531-11D0-A980-0020182A7050}", 
                                 &clsid);
       
       if(SUCCEEDED(hRes))
       {
                          hRes = AfxGetClassIDFromString(
                               "{8A544DC5-F531-11D0-A980-0020182A7050}", &iid);
    
                           if(SUCCEEDED(hRes))
                             SetNewData(clsid, iid); 
       }
    }

    Метод SetNewData, показанный ниже, создает instance в GasTankLevelGetter COM объекте или FishTankLevelGetter COM объекте в зависимости от CLSID. После этого SetNewData вызывает методы интерфейса ILevelGetter для получения данных.

    //--------------------------------------------------------------------
    void CLevelViewerDlg::SetNewData(const CLSID& clsid, const IID& iid)
    {
       ClearMembers();
    
       ASSERT(m_pILevelGetter == NULL);
    
       HRESULT hRes = CoCreateInstance(clsid, NULL, CLSCTX_ALL, 
                                     iid, (void**)&m_pILevelGetter);
    
           if(!SUCCEEDED(hRes))
       {
          m_pILevelGetter = NULL;
          return;
       }
    
       long lLowestSafeLevel, lHighestSafeLevel, lCurrentLevel;
       BSTR bstrMessage = NULL;
    
       m_pILevelGetter->GetLowestPossibleSafeLevel(&lLowestSafeLevel);
       m_pILevelGetter->GetHighestPossibleSafeLevel(&lHighestSafeLevel);
       m_pILevelGetter->GetCurrentLevel(&lCurrentLevel);
       m_pILevelGetter->GetTextMessage(&bstrMessage);
    
       m_pILevelGetter->Release();
       m_pILevelGetter = NULL;
    
       CString sLowest, sHighest, sCurrent, sMessage;
       sLowest.Format("%d",lLowestSafeLevel);
       sHighest.Format("%d",lHighestSafeLevel);
       sCurrent.Format("%d",lCurrentLevel);
       sMessage = bstrMessage;
       ::SysFreeString(bstrMessage);
    
       CString sItem;
       if(m_sLastCalled == _T("CheckedFish"))
       {
          //we are checking the fish tank now
          sItem = _T("Fish Tank");
       }
       else //m_sLastCalled == _T("CheckedGas")
       {
          //we are checking the fish tank now
          sItem = _T("Gas Tank");
       }
    
       CWnd* pWnd = GetDlgItem(IDC_TANK_TYPE);
       if(pWnd != NULL)
          pWnd->SetWindowText(sItem);
    
       pWnd = GetDlgItem(IDC_LOWEST_SAFE);
       if(pWnd != NULL)
          pWnd->SetWindowText(sLowest);
    
       pWnd = GetDlgItem(IDC_HIGHEST_SAFE);
       if(pWnd != NULL)
          pWnd->SetWindowText(sHighest);
    
       pWnd = GetDlgItem(IDC_CURRENT);
       if(pWnd != NULL)
          pWnd->SetWindowText(sCurrent);
    
       pWnd = GetDlgItem(IDC_MESSAGE);
       if(pWnd != NULL)
          pWnd->SetWindowText(sMessage);
    }

    Поскольку интерфейсы одинаковы, мы уверены, что методы будут работать с обоими COM объектами. Последние два шага должны реализовать OnFish и включить определение интерфейса.

    //--------------------------------------------------------------------
    void CLevelViewerDlg::OnFish() 
    {
       m_sLastCalled = _T("CheckedFish");
       CLSID clsid;
       IID   iid;   
       HRESULT hRes = AfxGetClassIDFromString(
          "{7F0DFAA3-F56D-11D0-A980-0020182A7050}", &clsid);
       
       if(SUCCEEDED(hRes))
          hRes = AfxGetClassIDFromString(
             "{7F0DFAA2-F56D-11D0-A980-0020182A7050}", &iid);
    
       if(SUCCEEDED(hRes))
          SetNewData(clsid, iid);
    }

    Определение интерфейса, созданное чисто виртуальными членами класса, включается в верхнюю часть файла описания реализации ( хотя его можно поместить в описание класса или отдельный .h файл), так что член класса m_pILevelGetter типа ILevelGetter* "знает" свои методы. Определение интерфейса представлено ниже:

    //------------------------------------------------------------------
    interface ILevelGetter : public IUnknown
    {
    public:
    virtual HRESULT STDMETHODCALLTYPE GetLowestPossibleSafeLevel(long* 
            plLowestSafeLevel) = 0;
    virtual HRESULT STDMETHODCALLTYPE GetHighestPossibleSafeLevel(long* 
            plLowestSafeLevel) = 0;
    virtual HRESULT STDMETHODCALLTYPE GetCurrentLevel(long* plLowestSafeLevel) = 0;
    virtual HRESULT STDMETHODCALLTYPE GetTextMessage(BSTR* pbstrMessage) = 0;
    };

    Теперь мы готовы откомпилировать, слинковать и запустить приложение. Если вы запустили приложение, вы можете щелкнуть по какой-либо кнопке, чтобы переключить COM компоненты, или позволить таймеру переключать их автоматически каждые четыре секунды. И только теперь Ричи Рич сможет спокойно лететь на своем вертолете и следить за уровнем воды в своем аквариуме.

    Часть 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.