Свойство | Миллионеры | Горох | C++ | СОМ |
Съедобны | √ | √ | x | x |
Поддерживают наследование | √ | √ | √ | N |
Могут стать президентом | √ | x | x | x |
Поддерживает ли СОМ наследование? И да, и нет. То, что подразумевается в журнальных статьях - это наследование реализации, которое имеет место, когда класс наследует свой код или реализацию от базового класса. Этот вид наследования СОМ не поддерживается. Однако СОМ поддерживает наследование интерфейса, т. е. наследование классом типа или интерфейса базового класса.
Многие необоснованно утверждают, что СОМ - плохая технология, так как она не поддерживает наследование реализации. Их аргументация напоминает мне о бурных «войнах» в Интернете - OS/2 против Windows, vi против Emacs, Java против Python и т. д. Я не участвую в таких спорах, ибо это пустая трата времени.
СОМ не поддерживает наследование реализации, потому что наследование реализации слишком тесно привязывает один объект к реализации другого. Если изменится реализация базового объекта, то производные объекты не будут работать, и их тоже нужно будет изменять. Для программы среднего размера на C++ это не очень трудно - у Вас есть доступ ко всем исходным текстам, и Вы можете изменить производные классы. Однако для программ большего размера на изменение всех зависимых классов может уйти недопустимо много времени. Хуже того, у Вас может даже не быть доступа к исходным текстам. Именно поэтому эксперты по созданию больших программ на C++ настоятельно рекомендуют строить приложения на фундаменте абстрактных базовых классов.
Не случайно базовые классы, т. е. род наследования интерфейсов в чистом виде, оказались также и способом реализации интерфейсов СОМ. Компоненты СОМ могут быть написаны кем угодно, где угодно и на каком угодно языке. Следовательно, необходимо очень тщательно защищать клиентов компонента от изменений. Наследование реализации не обеспечивает такой защиты.
Поэтому, чтобы гарантировать продолжение работы существующих приложений в случае изменений в компонентах, СОМ не поддерживает наследование реализации. Но из-за этого не теряются никакие функциональные возможности - ведь наследование реализации можно целиком смоделировать путем включения компонентов. Конечно, наследование реализации удобнее. Однако я полагаю, что большинство разработчиков предпочтут более устойчивую систему немного более удобной.
У наследования реализации своя область применения. В следующей главе я буду использовать его, чтобы избавиться от необходимости реализовывать IUnknown для каждого компонента. Но в этой главе мы остановимся на включении компонентов.
Может быть, это национальная особенность, но, по-моему, мало кто в США доволен существующим положением дел. Мы всегда хотим все улучшить - поэтому постоянно изменяем все подряд, от прически до собственного дома. Так мы поступаем и с компонентами. После того, как кто-то дал Вам компонент, Вы наверняка захотите расширить или подстроить его под свои задачи. Кроме того, Вы можете захотеть использовать новый, усовершенствованный компонент; В C++ подстройка реализуется с помощью включения и наследования. В СОМ компоненты подстраиваются (специализируются) с помощью включения (containment) и агрегирования (aggregation).
Включение и агрегирование - это Приемы программирования, в которых один компонент использует другой. Я называю эти два компонента внешним (outer component) и внутренним (inner component) соответственно. Внешний компонент или агрегирует, или включает в себя внутренний.
Возможны различные сочетания включения и агрегирования. Компонент может специализировать или расширять множество интерфейсов, реализованных многими разными компонентами. Некоторые интерфейсы он может включать, другие же агрегировать.
Поскольку агрегирование - это особый случай включения, мы рассмотрим включение первым.
Включение компонента выполняется столь же просто, как и использование. Каталог \CHAP08\CONTAIN на прилагающемся к книге компакт-диске содержит пример кода включения. В этом примере Компонент 1 - внешний; он реализует два интерфейса: IX и IY. При этом он использует реализацию IY Компонентом 2 - внутренним, включаемым компонентом. Это в точности соответствует схеме рис. 8-2.
Клиент и внутренний компонент - практически те же самые, что и клиент с компонентом из предыдущей главы. При включении одного компонента другим ни от клиента, ни от внутреннего компонента не требуется никаких специальных действий. Они даже не знают о самом факте использования включения.
Нам остается рассмотреть только внешний компонент - Компонент 1, который включает Компонент 2. В приведенном ниже листинге 8-1 показано объявление и большая часть реализации Компонента 1. Я выделил полужирным некоторые участки кода, относящиеся к включению. Новая переменная-член m_pIY содержит указатель на интерфейс IY включаемого Компонента 2.
Код включения из CONTAIN\CMPNT1
// // Интересные места кода обозначены @N. // /////////////////////////////////////////////////////////// // // Компонент 1 // class CA : public IX, public IY //@N { public: // IUnknown virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv) ; virtual ULONG __stdcall AddRef() ; virtual ULONG __stdcall Release() ; // Интерфейс IX virtual void __stdcall Fx() { cout << "Fx" << endl ;} // Интерфейс IY virtual void __stdcall Fy() { m_pIY->Fy() ;} //@N // Конструктор CA() ; // Деструктор ~CA() ; // Инициализация компонента путем //создания включаемого компонента. HRESULT __stdcall Init() ; //@N private: // Счетчик ссылок long m_cRef ; // Указатель на интерфейс IY включаемого компонента IY* m_pIY ; } ; // // Конструктор // CA::CA() : m_cRef(1), m_pIY(NULL) //@N { ::InterlockedIncrement(&g_cComponents) ; } // // Деструктор // CA::~CA() { ::InterlockedDecrement(&g_cComponents) ; trace("Самоликвидация.") ; // Освободить включаемый компонент. @N if (m_pIY != NULL) { m_pIY->Release() ; } } // Инициализация компонента путем // создания включаемого компонента. HRESULT __stdcall CA::Init() { trace(" Создать включаемый компонент.") ; HRESULT hr = ::CoCreateInstance(CLSID_Component2, NULL, CLSCTX_INPROC_SERVER, IID_IY, (void**)&m_pIY) ; if (FAILED(hr)) { trace("He могу создать включаемый компонент.") ; return E_FAIL ; } else { return S_OK ; } }Давайте рассмотрим, как работает этот код внешнего Компонента 1. Новый метод под названием Init создает внутренний Компонент 2 тем же самым способом, которым создают компоненты все клиенты, - посредством вызова CoCreateInstance. При этом внешний компонент запрашивает указатель на IY у внутреннего и, в случае успеха, сохраняет его в m_pIY.
В приведенном листинге не показаны реализация QueryInterface и функций внешнего IUnknown. Она абсолютно та же, что и в случае, когда включение не используется. Когда клиент запрашивает у Компонента 1 интерфейс IY, тот возвращает указатель на свой интерфейс. Затем, когда клиент вызывает метод этого интерфейса, Компонент 1 передает вызов Компоненту 2. Это выполняет следующая строка:
virtual void Fy() { m_pIY->Fy() ; }Когда Компонент 1 самоликвидируется, его деструктор вызывает Release для указателя m_рIY, в результате чего Компонент 2 также удаляет себя.
Фабрика класса Компонента 1 мало изменилась по сравнению с фабрикой класса из предыдущей главы. Единственный новый момент - то, что функция CreateInstance вызывает после создания Компонента 1 его функцию Init: Код этой функции приведен в листинге 8-2.
Код функции Createlnstance из CONTAIN\CMPNT1
HRESULT __stdcall CFactory::CreateInstance(IUnknown* pUnknownOuter, const IID& iid, void** ppv) { // Агрегирование не поддерживается. if (pUnknownOuter != NULL) { return CLASS_E_NOAGGREGATION ; } // Создать компонент. CA* pA = new CA ; if (pA == NULL) { return E_OUTOFMEMORY ; } // Инициализировать компонент. @N HRESULT hr = pA->Init() ; if (FAILED(hr)) { // Ошибка при инициализации. Удалить компонент. pA->Release() ; return hr ; } // Получить запрошенный интерфейс. hr = pA->QueryInterface(iid, ppv) ; pA->Release() ; return hr ; }Вот и все, что необходимо для реализации включения. Теперь рассмотрим, для Чего включение может применяться.
Рассмотрим пример. Имеется класс IAirplane (Самолет), который Вы хотите превратить в IFloatPlaw (Гидросамолет). Определения интерфейсов приводятся ниже:
interface IAirplane: IUnknown { void Take0ff(); void Fly(); void Land(); }; interface IFloatPlane: IAirplane { void LandingSurface(UINT iSurfaceType); void Float(); void Sink(); void Rust(); void DrainBankAccount(); };Предположим, что IAirplane уже реализован в компоненте MyAirplane. Внешний компонент может просто включить MyAirplane и использовать его интерфейс IAirplane для реализации членов IAirplane, которые наследует интерфейс IFloatPlane:
void CMyFloatPlane::Fly() { m_pIAirplane->Fly() ; }Другие члены IAirplane, вероятно, потребуется модифицировать, чтобы поддерживать взлет и посадку на воду:
void CMyFloatPlane::Land() { if (m_iLandingSurface == WATER) { WaterLanding(); } else { m_pIAirplane->Land() ; } }Как видите, использовать включение в данном случае просто. Однако если в интерфейсе IAirplane много членов; то написание кода, передающего вызовы от клиента в MyAirplane, будет утомительным. По счастью, это не вопрос поддержки, так как после обнародования интерфейса он не изменяется. Агрегирование дает некоторую поблажку ленивому программисту, который не хочет реализовывать код для передачи вызовов внутренним объектам. Однако при использовании агрегирования к интерфейсу нельзя добавить свой код. Перейдем теперь к агрегированию.
Рассмотрим, как работает агрегирование. Клиент запрашивает у внешнего компонента интерфейс IY. Вместо того, чтобы реализовывать IY, внешний компонент запрашивает у внутреннего его интерфейс IY, указатель которого и возвращает клиенту. Когда клиент использует интерфейс IY, он напрямую вызывает функции-члены ГУ, реализованные внутренним компонентом. В работе клиента с этим интерфейсом внешний компонент не участвует, полный контроль над интерфейсом IY принадлежит внутреннему компоненту. Хотя поначалу агрегирование кажется простым, как мы увидим далее, правильная реализация интерфейса IUnknown внутреннего компонента представляет некоторые трудности. Магия агрегирования заключена в реализации QueryInterface. Давайте реализуем QueryInterface для внешнего компонента так, чтобы тот возвращал указатель на объект внутреннего.
C++ и агрегирование |
В C++ нет средства, эквивалентного агрегированию. Агрегирование - это динамический вид наследования, тогда как наследование C++ всегда статично. Наилучший способ моделирования агрегирования в C++ - это переопределение операции разыменования (operators). Эта техника будет рассматриваться при реализации smart-указателей в следующей главе. Переопределение operator -> связано с большими ограничениями, чем агрегирование СОМ. Вы можете передавать вызовы только одному классу, тогда как в СОМ можно агрегировать сколько угодно интерфейсов. |
class СА : public IX { public: //IUnknown virtual HRESULT __stdcall QueryInterface(const IID& iid, void**ppv) ; virtual ULONG __stdcall AddRef(); virtual ULONG __stdcall Release(); // Интерфейс IX virtual void __stdcall Fx() { cout « "Fx" « endl ;} // Конструктор CA() ; // Деструктор ~CA() ; // Функция инициализации, вызываемая фабрикой класса для // создания включаемого компонента. HRESULT Init() ; private: // Счетчик ссылок long m_cRef ; // Указатель на IUnknown внутреннего компонента IUnknown* in_pUnknownInner; };Обратите внимание, что по внешнему виду объявленного компонента нельзя сказать, что он поддерживает интерфейс IY: он не наследует IY и не реализует какие-либо его члены. Этот внешний компонент использует реализацию IY внутреннего компонента. Основные действия внешнего компонента происходят внутри его функции QueryInterface, которая возвращает указатель на интерфейс внутреннего объекта. В приведенном ниже фрагменте кода переменная-член m_pUnknownInner содержит адрес IUnknown внутреннего компонента.
HRESULT __stdcall CA::QueryInterface(const IID& iid, void** ppv) { if(iid == IID_IUnknown) { *ppv = static_castВ этом примере QueryInterface внешнего компонента просто вызывает QueryInterface внутреннего. Все очень хорошо и просто, но если бы это еще правильно работало!(this); } else if (iid == IID_IX) { *ppv = static_cast (this) ; } else if (iid == IID_IY) { return m_pUnknownInner->QueryInterface(iid, ppv); } else { *ppv = NULL ; return E_NOINTERFACE ; } reinterpret_cast (*ppv)->AddRef(); return S_OK ; }
Проблема заключается не в приведенном коде - она в интерфейсе IUnknown внутреннего компонента. Агрегированный внутренний компонент должен обрабатывать вызовы функций членов QueryInterface особым образом. Как мы увидим, здесь фактически необходимы две реализации IUnknown.
Теперь давайте рассмотрим, почему обычная реализация IUnknown для внутреннего компонента вызывает проблемы. Затем познакомимся с тем, как для их устранения реализуются два интерфейса IUnknown. Я покажу также, как модифицировать создание внутреннего компонента, чтобы оба компонента получили нужные им указатели. Наконец, мы посмотрим, как внешнему компоненту передаются указатели на интерфейсы внутреннего (это более сложный процесс, чем Вам может показаться).
HRESULT __stdcall CoCreateInstance( const CLSID& clsid, IUnknown* pUnknownOuter, // Внешний компонент DWORD dwClsContext, // Контекст сервера const IID& iid, void** ppv ); HRESULT __stdcall CreateInstance( IUnknown* pUnknownOuter, const IID& iid, void** ppv );Внешний компонент передает указатель на свой интерфейс IUnknown внутреннему компоненту с помощью параметра pUnknownOuter. Если указатель на внешний IUnknown не равен NULL, то компонент агрегируется. Испольяуя указатель на IUnknown, переданный в Createlnstance, компонент узнает, регируется ли он и кто его агрегирует. Если компонент не агрегируется, он использует собственную реализацию IUnknown. В противном случае он должен делегировать вызовы внешнему IUnknown.
Из рис. 8-5 видно, что часть интерфейса IY, относящаяся к IUnknown, переправляет вызовы делегирующему IUnknown. Делегирующий IUnknown вызывает неделегирующий. Последний в действительности и реализует интерфейс IUnknown обычным образом.
struct INondelegatingUnknown { virtual HRESULT __stdcall NondelegatingQueryInterrace(const IID&, void**) = 0 ; virtual ULONG __stdcall NondelegatingAddRef() = 0 ; virtual ULONG __stdcall Nondeleg'atingReleaseO = 0 ; } ;Методы NondelegatingAddRef и NondelegatingRelease интерфейса NondekgatingUnknown реализованы в точности так же, как ранее были реализован AddRef и Release для IUnknown. Однако в NondelegatingQueryInterface есть небольшое, но очень важное изменение.
HRESULT __stdcall CB::NondelegatingQueryInterface(const IID& iid, void** ppv) { if (iid == IID_IL)nknown) { *ppv = static_castОбратите внимание на приведение типа указателя this внутреннего компонента к INondelegatingUnknown. Это приведение очень важно. Преобразуя this к INondelegatingUnknown, мы гарантируем, что будет возвращен неделегирующий IUnknown. Неделегирующий IUnknown всегда возвращает указатель на себя, если у него запрашивается IID_IUnknown. Без этого приведения типа вместо неделегирующего IUnknown возвращался бы делегирующий. Когда компонент агрегируется, делегирующий IUnknown передает все вызовы QueryInterface, Release и AddRef внешнему объекту.(this); } else if (iid == IID_IY) { *ppv = static_cast (tnis) ; } else { *ppv = NULL ; return E_NOINTERFACE ; } reinterpret_cast (*ppv)->AddRef() ; return S_OK ; }
Клиенты агрегируемого компонента никогда не получают указатели на неделегирующий IUnknown внутреннего компонента. Всякий раз, когда клиент запрашивает указатель на IUnknown, он получает IUnknown внешнего компонента. Указатель на неделегирующий IUnknown внутреннего компонента передается только внешнему компоненту. Теперь рассмотрим, как реализовать делегирующий IUnknown.
class CB : public IY, public INondelegatingUnknown { public: // Делегирующий IUnknown virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv) { // Делегировать QueryInterface. return m_pUnknownOuter->QueryInterface(iid, ppv) ; } virtual ULONG __stdcall AddRefQ { // Делегировать AddRef. return m_pUnknownOuter->AddRef() ; } virtual ULONG __stdcall Release() { // Делегировать Release. return m_p UnknownOuter->Release() ; } // Неделегирующий IUnknown virtual HRESULT __stdcall NondelegatingQueryInterface(const IID& iid, void** ppv) ; virtual ULONG __stdcall NondelegatingAddRef() ; virtual ULONG __stdcall NondelegatingReleaseO; // Интерфейс IY virtual void __stdcall Fy() { cout « "Fy" « endl ;} // Конструктор CB(IUnknown* m_pUnknownOuter) ; // Деструктор ~CB(); private: long m_cRef ; IUnknown* m_pUnknownOuter ; };
Кроме того, отметьте себе, что пятый параметр запрашивает у внутреннего компонента указатель на IUnknown. Фабрика класса будет возвращать указатель на неделегирующий IUnknown внутреннего компонента. Как мы уж видели, этот указатель необходим внешнему компоненту для передачи вызовов QueryInterface внутреннему компоненту. Здесь внешний компонент обязан запрашивать указатель на IUnknown; в противном случае он никогда не сможет получить его. Внешний компонент должен сохранить неделегирующий IUnknown внутреннего компонента для последующего использования.
В данном примере нет необходимости явно приводить указатель this к указателю на IUnknown, так как СА наследует только IX, и, следовательно, неявное преобразование не будет неоднозначным.
HRESULT CA::Init() { IUnknown* pUnknownOuter = this'; HRESULT hr = CoCreateInstance(CLSID_Component2, pUnknownOuter, CLSCTX_INPROC_SERVER, IID_IUnknown, (void**)&m_pUnknownInner) ; if (FAILED(hr)) { return E_FAIL ; } return S_OK; }Реализация IClassFactory::CreateInstance внешнего компонента вызывает CA::Init. В других отношениях реализация IClassFactory внешнего компонента остается неизменной. Фабрика же класса внутреннего компонента подверглась некоторым изменениям, которые мы теперь и рассмотрим.
HRESULT __stdcall CFactory::CreateInstance(IUnknown* pUnknownOuter, const IID& iid, void** ppv) { // При агрегировании iid должен быть IID_IUnknown. if ((pUnknownOuter != NULL) && (iid != IID_IUnknown)) { return CLASS_E_NOAGGREGATION ; } // Создать компонент. СВ* рВ = new CB(pUnknownOuter) ; if (pB == NULL) { return E_OUTOFMEMORY; } // Получить запрошенный интерфейс. HRESULT hr = pB->NondelegatingQueryInterface(iid, ppv) ; pB->NondelegatingRelease() ; return hr ; }Для получения запрашиваемого интерфейса вновь созданного внутреннего компонента показанная выше функция Createlnstance вызывает не QueryInterface, a NondelegatingQueryInterface. Если внутренний компонент агрегируется, вызовы QueryInterface он будет делегировать внешнему IUnknown. Фабрика класса должна возвратить указатель на неделегирующий QueryInterface, поэтому он вызывает NondelegatingQueryInterface.
СВ::CB(IUnknown* pUnknownOuter) : m_cRef(1) { ::InterlockedIncrement(&g_cComponents) ; if (pUnknownOuter == NULL) { // He агрегируется; использовать неделегирующий IUnknown. iti_pUnknownOuter = reinterpret_cast(static_cast (this)) ; } else { // Агрегируется; использовать внешний IUnknown. m_pUnknownOuter = pUnknownOuter ; } }
Но здесь необходимо быть аккуратным и не запутаться. Когда Вы вызывате QueryInterface, чтобы получить m_pUnknownInner указатель на интерфейс IID_IY, эта функция, как примерный школьник, вызывает для возвращаемого указателя AddRef. Поскольку внутренний компонент агрегирован, он делегирует вызов AddRef внешнему IUnknown. В результате увеличивается счечик ссылок внешнего компонента, а не внутреннего. Я хочу еще раз это подчеркнуть. Когда внешний компонент запрашивает интерфейс через указатель на неделегирующий IUnknown или на какой-либо еще интерфейс внуреннего компонента, счетчик ссылок внешнего компонента увеличивается. Это именно то, что требуется, когда интерфейс через указатель на интерфейс внутреннего компонента запрашивает клиент. Но в данном случае указатель на интерфейс IY запрашивает внешний компонент, и счетчик ссылок для этого указателя является счетчиком ссылок внешнего компонента. Таким образом, внешний компонент удерживает одну ссылку сам на ceбя! Если допустить такое, счетчик ссылок внешнего компонента никогда не станет нулем, и компонент никогда не будет удален из памяти.
Так как время существования указателя на IY, принадлежащего внешнему компоненту, вложено во время существования самого внешнего компонента, нам нет необходимости увеличивать счетчик ссылок. Но не вызывайте для уменшения счетчика ссылок Release для IY - мы обязаны работать с интерфейсе IY так, как если бы у него был отдельный счетчик ссылок. (В нашей реализации компонента неважно, для какого указателя вызывать Release. Но в других случаях это может быть не так.) Освобождение интерфейса IY может освободить используемые им ресурсы. Следовательно, общее правило состоит в том чтобы вызывать Release, используя указатель, переданный в CoCreateInstance Версия CA::Init, запрашивающая интерфейс IY, приведена ниже:
HRESULT __stdcall CA::Init() { // Получить указатель на внешний IDnknown. IDnknown* pUnknownOuter = this ; // Создать внутренний компонент. HRESULT hr = ::CoCreateInstance(CLSID_Component2, pUnknownOuter, // IUnknown внешнего // компонента CLSCTX_INPROC_SERVER, IID_IUnknown, // При агрегировании только // IUnknown (void**)&m_pUnknownInner) ; if (FAILED(hr)) { // Ошибка при создании компонента. return E_FAIL ; } // Этот вызов увеличит счетчик ссылок внешнего компонента. // Получить интерфейс IY внутреннего компонента. hr = m_pUnknownInner->QueryInterface(IID_IY, (void**)&m_pIY) ; if (FAILED(hr)) { // Внутренний компонент не поддерживает интерфейс IY. m_pUnknownInner->Release() ; return E_FAIL ; } // Нам нужно уменьшить счетчик ссылок на внешний компонент, // увеличенный предыдущим вызовом, pUnknownOuter->Release() ; return S_OK ; }При реализации QyeryInterface у нас есть выбор - либо возвращать m_pIY, либо вызывать QyeryInterface внутреннего компонента. Поэтому можно использовать либо
else if (iid == IIO_IY) { return m_pUnknownOuter->QueryInterface(iid, ppv) ; }как мы это делали, либо
else if (iid == IID_IY) { *ppv = m_pIY ; }Итак, мы уже создали внутренний компонент, запросили у него интерфейс, скорректировали счетчик ссылок и вернули интерфейс клиенту. Чего мы еще не сделали, так это не освободили интерфейс в деструкторе внешнего компонента. Мы не можем просто вызвать m_pIY->Release, так как у нас нет для него подсчитанной ссылки. Мы убрали ее в функции Init внешнего компонента после того, как получили указатель на IY. Tenepь необходимо повторить процедуру в обратном порядке, восстановив счетчик ссылок и вызвав Release для указателя на IY. Однако здесь следует быть осторожным, так как в противном случае этот последний вызов Releasе снова сделает счетчик ссылок внешнего компонента нулевым, и тот попытается удалить себя.
Таким образом, процесс освобождения нашего указателя на интерфейс внутреннего компонента состоит из трех этапов. Во-первых, необходимо гарантировать, что наш компонент не попытается снова удалить себя. Во-вторых, необходимо вызвать AddRef для внешнего компонента, так как любой вызов Release для внутреннего компонента вызовет Release для внешнего. И, наконец, мы можем освободить указатель на IY, хранящийся во внешнем компоненте. Соответствующий код приведен ниже:
// 1. Увеличить счетчик ссылок во избежание // рекурсивного вызова деструктора. in_cRef = 1 ; // 2. AddRef для внешнего IUnknown. IUnknown* pUnknownOuter = this ; PUnknownOuter->AddRef() ; // 3. Освободить интерфейс. m_pIY->Release() ;Давайте мысленно проследим работу этого кода внешнего компонента. Первое, что мы делаем, - устанавливаем счетчик ссылок в 1. Далее мы увеличиваем его до двух. Затем вызываем Release для интерфейса IY. Внутренний компонент будет делегировать этот вызов внешнему. Последний уменьшит счетчик ссылок с 2 до 1. Если бы мы не установили ранее счетчик ссылок в 1, то компонент попытался бы во второй раз удалить себя. В нашей реализации, когда внутренний компонент агрегируется, его функция Release просто делегируется внешнему IUnknown. Однако внешний компонент должен работать с внутренним так, как если бы тот вел отдельные счетчики ссылок для каждого интерфейса, поскольку другие реализации внутреннего компонента могут делать больше, чем просто делегировать Release внешнему компоненту. Внутренний компонент может также освобождать некоторые ресурсы или выполнять другие операции. Большая часть этого кода может показаться избыточной и ненужной. Но если внешний компонент сам агрегирован другим компонентом, выполнение описанных выше шагов становится критически важным. В примере из гл. 9 показан компонент, который агрегирует компонент, агрегирующий другой компонент.
«Вот и все, что необходимо сделать для реализации агрегирования»,- сказал он, улыбаясь. В действительности, после того, как Вы написали корректный код агрегирования, он работает отлично, и о нем можно забыть. Однако после первой попытки его написать многие начинают называть его не «aggregation», но «aggravation»** (Прим. перев. - Aggravation 1. Ухудшение, усугубление; 2. Раздражение, огорчение.)
Реализуем компонент, который агрегирует некий интерфейс. В данном примере Компонент 1 поддерживает два интерфейса, так же как и в примере с включением. Однако здесь он реализует только IX. Он не будет ни реализовывать IY, ни передавать его вызовы реализации этого интерфейса Компонентом 2. Вместо этого, когда клиент запрашивает у Компонента 1 интерфейс IY, Компонент 1 возвращает указатель на интерфейс IY, реализуемый внутренним Компонентом 2. В листинге 8-3 представлен внешний компонент, а в листинге 8-4 - внутренний. Клиент остался практически неизменным; ему совершенно неважно, используем мы агрегирование или включение.
// // Cmpnt1.cpp - Компонент 1 // #include#include #include "Iface.h" #include "Registry.h" void trace(const char* msg) { cout « "Component 1:\t" « msg « endl ;} //////////////////////////////////////////////////////////// // // Глобальные переменные // // Статические переменные static HMODULE g_hModule = NULL ; // Описатель модуля DLL static long g_cComponents = 0; // Счетчик активных компонентов static long g_cServerLocks = 0 ; // Число блокировок // Дружественное имя компонента const char g_szFrlendlyName[] = "Inside СОМ, Chapter 8 Example 2, Component 1" ; // Не зависящий от версии ProgID const char g_szVerIndProgID[] = "InsideCOM.ChapOS. Ex2.Cmpnt1" ; // ProgID const char g_szProgID[] = "InsideCOM.ChapOS.Ex2.Cmpnt1.1" ; /////////////////////////////////////////////////////////// // // Компонент А // class CA : public IX // public IY { public: // IUnknown virtual HRESULT __stdcall QueryInterface(const IID& iid, void** Ppv) ; virtual ULONG __stdcall AddRef() ; virtual ULONG __stdcall Release() ; // Интерфейс IX virtual void __stdcall Fx() { cout « "Fx" « endl ;} /* Компонент 1 агрегирует интерфейс IY, а не реализует его. // Интерфейс IY virtual void __stdcall Fy() { m_pIY->Fy() ;} */ // Конструктор CA() ; // Деструктор ~СА() ; // Функция инициализации, вызываемая фабрикой класса для // создания агрегируемого компонента, HRESULT __stdcall Init() ; private: // Счетчик ссылок long m_cRef; // Указатель на интерфейс IY агрегированного компонента. // (Нам необязательно сохранять указатель на IY. Однако мы // можем использовать его в QueryInterface.) IY* m_pIY ; // Указатель на IUnknown внутреннего компонента. IUnknown* m_pUnknownInner ; // // Конструктор // СА::СА() : m_cRef(1), m_pUnknownInner(NULL) { ::InterlockedIncrement(&g_cCoinponents) ; } // // Деструктор // СА::~СА() { ::InterlockedDecrement(&g_cComponents) ; trace("Самоликвидация."); // Предотвращение рекурсивного вызова деструктора следующей // ниже парой AddRef/Release. m_cRef = 1 ; // Учесть pUnknownOuter->Release в методе Init. IUnknown* pUnknownOuter = this ; pUnknownOuter->AddRef() ; // Правильное освобождение указателя; возможен поинтерфейсный // подсчет ссылок. m_pIY->Release() ; // Освободить внутренний компонент- if (m_pUnknownInner l= NULL) { m_pUnknownInner->Release() ; } } // Инициализировать компонент путем создания внутреннего компонента HRESULT __stdcall CA::Init() { //Получить указатель на внешний IDnknown. // Поскольку этот компонент не агрегируется, внешний IUnknown - // это то же самое, что и указатель this. IDnknown* pUnknownOuter = this ; trace("Создать внутренний компонент".) ; HRESULT hr = ::CoCreateInstance(CLSID_Component2, pUnknownOuter, // Unknown внешнего // компонента CLSCTX_INPROC_SERVER, IID_Iunknown, // При агрегировании - // Unknown (void**)&m_pUnknownInner) ; if (FAILED(hr)) { trace("He могу создать внутренний компонент.") ; return E_FAIL ; } // Следующий вызов будет увеличивать счетчик ссылок внешнего компонента. trace("Получить интерфейс IY внутреннего компонента.") ; hr = m_pUnknownInner->QueryInterface(IID_IY, (void**)&m_pIY) ; if (FAILED(hr)) { trace("Внутренний компонент не поддерживает интерфейс IY.") m_pUnknownInner->Release() ; return E_FAIL ; } // Необходимо уменьшить счетчик ссылок внешнего компонента, увеличенный // предыдущим вызовом. Для этого вызываем Release для указателя, // переданного CoCreateInstance. pUnknownOuter->Release() ; return S_OK ; } // // Реализация IUnknown // HRESULT __stdcall CA::QueryIhterface(const IID& iid, void** ppv) { if (iid == IID_IUnknown) { *ppv = static_cast (this) ; } else if (iid == IID_IX) { *ppv = static_cast (this) ; } } else if (iid == IID_IY) { trace("Bepнyть интерфейс IY внутреннего компонента.") ; #if 1 // Этот интерфейс можно запросить... return m_pllnknownInner->QueryInterface(iid, ppv) ; #else // либо можно вернуть сохраненный указатель. *ppv = m_pIY ; // Проходим дальше, чтобы была вызвана AddRef. #endif } else { *ppv = NULL ;, return E_NOINTERFACE ; } reinterpret_cast (*ppv)->AddRef() ; return S_OK ; } ULONG __stdcall CA::AddRef() { return : :InterlockedIncrernent(&m_cRef) ; } ULONG __stdcall CA::Release() { if (: :InterlockedDecrement(&m_cRef) == 0) { delete this ; return 0 ; return in_cRef ; } /////////////////////////////////////////////////////////// // // Фабрика класса // class CFactory : public IClassFactory { public: // IUnknown virtual HRESULT __st,dcall Ouerylnterface(const IIO& iid, void** Ppv) ; virtual ULONG __stdcall AddRef() ; virtual ULONG __stdcall Release() ; // Интерфейс IClassFactory virtual HRESULT __stdcall CreateInstance(IUnknown* pUnknownOuter, const IID& iid, void** ppv) ; virtual HRESULT __stdcall LockServer(BOOL bLock) ; // Конструктор . CFactory() :: m_cRef(1) {} // Деструктор ~CFactory(){} private: long m_cRef ; } ; // // Реализация IUnknown для фабрики класса. // HRESULT __stdcall CFactory::QueryInterface(REFIIO iid, void** ppv) { IUnknown* pi ; if ((iid == IID_IUnknown) |] (iid == IID_IClassFactory)) { pi = static_cast (this) ; } else { *ppv = NULL ; return E_NOINTERFACE ; } pI->AddRef() ; *ppv = pi ; return S_OK ; } ULONG __stdcall CFactory::AddRef() { return : :Interl'ockedIncrement(&m_cRef) ; } ULONG __stdcall CFactory::Release() { if (: :InterlockedDecrenient(&m_cRef) == 0) { delete this ; return 0; } return m_cRef ; } // // Реализация IClassFactory // HRESULT __stdcall CFactory::CreateInstance(IUnknown* pUnknownOuter const IID& iid, void** ppv) { // Агрегирование не поддерживается. if (pUnknownOuter != NULL) { return CLASS_E_NOAGGREGATION ; } // Создать компонент. CA* рА = new CA ; if (pA == NULL) { return E_OUTOFMEMORY ; } // Инициализировать компонент. HRESULT hr = pA->Init() ; if (FAILED(hr)) { // Ошибка инициализации. Удалить компонент. pA->Release() ; return hr ; } // Получить запрошенный интерфейс. hr = pA->QueryInterface(iid, ppv) ; pA->Release() ; return hr ; } // LockServer HRESULT __stdcall CFactory::LockServer(BOOL bLock) { if (bLock) { ::InterlockedIncrement(&g_cServerLocks) ; } else { ::InterlockedDecrement(&g_cServerLocks) ; } return S_OK ; } ///////////////////////////////////////////////////// // // Экспортируемые функции // STDAPI DllCanUnloadNow() { if ((g_cComponents == 0) && (g_c8erverLocks == 0)) { return S_OK ; } else return S_FALSE ; } } // // Получение фабрики класса. // STDAPI DllGetClassObject(const CLSID& cisid, const IID& iid, void** ppv) { // Можем ли мы создать такой компонент? if (cisid != GLSID_Component1) { return CLASS_E_CLASSNOTAVAILABLE ; } // Создать фабрику класса. CFactory* pFactory = new CFactory ; // В конструкторе нет AddRe' if (pFactory == NULL) { return E_OUTOFMEMORY ; } // Получить запрошенный интерфейс. HRESULT hr = pFactory->Query!nterface(iid, ppv) ; pFactory->Release() ; return hr ; } // // Регистрация сервера // STDAPI DllRegisterServer() { return RegisterServer(g_hModule, CLSID_Component1, g_szFriendlyName, g_szVerIndProgID, g_szProgID) ; } STDAPI DllUnregisterServer() { return UnregisterServer(CLSID_Component1, g_szVerIndProgID, g_szProgID) ; } ////////////////////////////////////////////////// // // Информация о модуле DLL // BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* IpReserved) { if (dwReason == DLL_PROCESS_ATTACH) { g_hModule = hModule ; } return TRUE ; } // Cmpnt2.cpp - Компонент 2 // #include #include #include "Iface.h" #include "Registry.h" void trace(const char* msg) { cout « "Component 2:\t" « msg « endl ;} //////////////////////////////////////////////////////////////////// // // Глобальные переменные // // Статические переменные static HMODULE g_hModule = NULL ; // Описатель модуля DLL static long g_cCoinponents = 0 ; // Счетчик активных компонентов static long g_c3erverLocks = 0 ; // Счетчик блокировок // Дружественное имя компонента const char g_szFriendlyName[] = "Inside СОМ, Chapter 8 Example 2, Component 2" ; // Не зависящий от версии ProgID const char g_szVerIndProgID[] = "InsideCOM.ChapOB.Ex2.Cmpnt2" ; // ProgID const char g_szProgID[] = "InsideCOM.Chap.08. Ex2.Cmpnt2.1" ; //////////////////////////////////////////////////////////////////// // // Неделегирующий интерфейс IUnknown // struct INondelegatingUnknown { virtual HRESULT __stdcall Nondelegating0uerylnterface(const IID&, void**) = 0 ; virtual ULONG __stdcall NondelegatingAddRef() = 0 ; virtual ULONG __stdcall NondelegatingRelease() = 0 ; }; //////////////////////////////////////////////////////////////////// // // Компонент // class CB : public IY, public INondelegatingUnknown { public: // Делегирующий IUnknown virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv) { trасе("Делегировать QueryInterface.") ; return m_pUnknownOuter->QueryInterface(iid, ppv) ; } virtual ULONG __stdcall AddRef() { trace("Делегировать AddRef.") ; return m_pUnknownOuter->AddRef() ; } virtual ULONG __stdcall Release() { trасе("Делегировать Release.") ; return m_pUnknownOuter->Release() ; } // Неделегирующий IUnknown virtual HRESULT __stdcall NondelegatingQueryInterface(const IID& iid, void** ppv) ; virtual ULONG __stdcall NondelegatingAddRef() ; virtual ULONG __stdcall NondelegatingRelease() ; // Интерфейс IY virtual void __stdcall Fy() { cout « "Fy" « endl ;} // Конструктор CB(IUnknown* m_pUhknownOuter) ; // Деструктор ~СВ() ; private: long m_cRef ; IUnknown* m_pIUnknownOuter; } ; // // Реализация IUnknown // HRESULT __stdcall CB::NondelegatingQueryInterface(const IID& lid, void** ppv) { if (iid == IID_IUnknown) { // !!! ПРИВЕДЕНИЕ ТИПА ОЧЕНЬ ВАЖНО !!! *ppv = static_cast (this) ; } else if (iid == IID_IY) { *ppv = static_cast (this) ; } else { *ppv = NULL ; return E_NOINTERFACE ; } reinterpret_cast (*ppv)->AddRef() ; return S_OK ; } ULONG __stdcall CB::NondelegatingAddRef() { return : :InterlockedIncre(nent(&in_cRef) ; } ULONG __stdcall CB::NondelegatingRelease() { if (::InterlockedDecrement(&m_cRef) == 0) { delete this ; return 0 ; } return in_cRef ; } // // Конструктор // CB::CB(IUnknown* pUnknownOuter) : m_cRef(1) { ::InterlockedIncrement(&g_cComponents) ; if (pUnknownOuter == NULL) { trace("He агрегируется; использовать неделегирующий IUnknown/'): rn_pUnknownOuter = reinterpret_cast (static_cast (this)) ; } else { trасе("Агрегируется; делегировать внешнему IUnknown.") ; m_pUnknownOuter = pUnknownOuter ; } } // // Деструктор // СВ::~СВ() { ::InterlockedDecrement(&g_cComponents) ; trace("Саморазрушение") ; } ////////////////////////////////////////////////////// // // Фабрика класса // class CFactory : public IClassFactory { public: // IUnknown virtual HRESULT __stdcall QueryInterface(const IID& iid, void** Ppv) ; virtual ULONG __stdcall AddRef(); virtual ULONG __stdcall Release() ; // Интерфейс IClassFactory virtual HRESULT __stdcall CreateInstance(IUnknown*pUnknownOuter. const IID& iid, void** ppv) ; virtual HRESULT __stdcall LockServer(BOOL bLock) ; // Конструктор CFactory() : m_cRef(1) {} // Деструктор ~CFactory() {} private: long m_cRef ; } ; // // Реализация IUnknown для фабрики класса // HRESULT __stdcall CFactory::QueryInterface(const IID& iid, void** PPV) { if ((lid == IID_IUnknown) || (iid == IID_IClassFactory)) { *ppv = static_cast (this) ; } else { *ppv = NULL ; return E_NOINTERFACE ; } reinterpret_cast (*ppv)->AddRef() ; return S_OK ; } ULONG __stdcall CFactory: :AddRef() { return ::InterlockedIncrement(&m_cRef) ; } ULONG __stdcall CFactory: :Release() { if (::InterlockedDecrement(&m_cRef) == 0) { delete this ; return 0 ; } return m_cRef ; } // // Peaлизация IClassFactory // HRESULT __stdcall CFactory::CreateInstance(IUnknown* pUnknownOuter, const IID& lid, void** ppv) { // При агрегировании iid должен быть IID_IUnknown. if ((pUnknownOuter != NULL) && (iid != IID_IUnknown)) { return CLASS_E_NOAGGREGATION ; } // Создать компонент. CB* рВ = new CB(pUnknownOuter) ; if (pB == NULL) { return E_OUTOFMEMORY ; } // Получить запрошенный интерфейс. HRESULT hr = pB->NondelegatingQueryInterface(iid, ppv) ; pB->NondelegatingRelease() ; return hr ; } // LockServer HRESULT __stdcall CFactory::LockServer(BOOL bLock) { if (bLock) { ::InterlockedIncrement(&g_cServerLocks) ; } else { ::InterlockedDecrement(&g_cServerLocks) ; } return S_OK ; } ////////////////////////////////////////////////////////// // Экспортируемые функции // STDAPI DllCanUnloadNow() { if ((g_cCo(nponents == 0) && (g_c8erverLocks == 0)) { return S_OK ; } else { return S_FALSE ; } } // // Получение фабрики класса. // STDAPI DllGetCrassObject(const CLSID& cisid, const IID& iid, void** ppv) { // Можем ли мы создать такой компонент? if (clsid != CLSID_Component2) { return CLASS_E_CLASSNOTAVAILABLE ; } // Создать фабрику класса. CFactory* pFactory = new CFactory ; // В конструкторе нет AddRef if (pFactory == NULL) { return E_OUTOFMEMORY ; } // Получить запрошенный интерфейс. HRESULT hr = pFactory->QueryInterface(iid, ppv) ; pFactory->Release() ; return hr ; } // // Регистрация сервера // STDAPI DllRegisterServer() { return RegisterServer(g_hModule, CLSID_Component2, g_szFrlendlyNaine, Q_szVerIndProgID, g_szPr^ID) ; } STDAPI DllUnregisterServer() { return unregisterServer(CLSID_Component2, g_szVerIndProgID, g_szProgID) ; } /////////////////////////////////////////////////////// // // Информация о модуле DLL // BOOL APIENTRY DllMaln(HANDLE hModule, DWORD dwReason, void* IpReserved) { if (dwReason == DLL_PROCESS_ATTACH) { g_hModule = hModule ; } return TRUE ; }
А что, если внешний компонент хочет агрегировать несколько интерфейсов внутреннего? Внешний компонент легко модифицировать так, чтобы поддерживать еще один интерфейс внутреннего компонента:
else if ((lid == IID_IY) || (iid == IID_IZ)) { return m_pIUnknownInner->QueryInterface(iid, ppv) ; }Конечно, для изменения кода внешнего компонента нам потребуется доступ к этому коду и компилятор. Но как быть, если мы хотим, чтобы клиент имел доступ ко всем интерфейсам внутреннего компонента, включая те, что будут добавлены после того, как внешний компонент будет написан, скомпилирован и поставлен заказчику? Все очень просто - удалите условие. Вместо проверки идентификатора интерфейса можно просто слепо передавать его внутреннему компоненту:
else if (iid == IID_IX) { *ppv = static_castДанная процедура называется слепым агрегированием (blind aggregation), так как внешний компонент слепо передает идентификаторы интерфейсов внутреннему. При слепом агрегировании внешний компонент не контролирует, какие из интерфейсов внутреннего компонента он предоставляет клиенту. В большинстве случаев лучше не прибегать к слепому агрегированию. Одна из причин этого в том, что внутренний компонент может поддерживать интерфейсы, не совместимые с интерфейсами, которые поддерживаются внешним компонентом. Например, внешний компонент для сохранения файлов может поддерживать интерфейс ISlowFile, тогда как внутренний для этой же цели поддерживает интерфейс IFastFile. Предположим, что клиент всегда запрашивает IFastFile прежде, чем запрашивать ISlowFile. Если внешний компонент слепо агрегирует внутренний, клиент получит указатель на IFastFile внутреннего компонента. Внутренний компонент ничего не знает о внешнем и поэтому, не будет правильно сохранять информацию, связанную с последним. Проще всего избежать таких конфликтов, не используя слепое агрегирование.(this) ; } else // Нет условия { return m_pllnknownInner->QueryInterface(iid, ppv) ; }
Если полное воздержание кажется излишне сильным условием, для устранения подобных конфликтов можно использовать два менее радикальных способа. Во-первых, при реализации внешнего компонента не реализуйте интерфейсы, которые могут дублировать функциональность интерфейсов внутреннего компонента. Во-вторых, можно создавать внешний компонент и клиент либо внешний и внутренний компонент как взаимосвязанные пары.
Предположим, что у нас есть программа преобразования растровых изображений. Пользователь может модифицировать растровое изображение c помощью различных алгоритмов. Последние реализованы как внутренний компонент, который пользователь может добавлять в систему во время работы. Каждый внутренний компонент может считывать и сохранять растровые образы, а также преобразовывать их в соответствии с некоторым особым алгоритмом. У внешнего компонента есть интерфейс ISetColors, устанавливающий цвета, с которыми работает внутренний компонент. Внешний компонент также имеет интерфейс IToolInfo, который отображает значки разных алгоритмов преобразования на панели инструментов и создает внутренний компонент, когда пользователь выбирает соответствующий значок (рис. 8-7). ISetColors - это пример обычного интерфейса, который расширяет абстракцию алгоритма преобразования. Компонент преобразования, вероятно, уже поддерживает интерфейс, например IColors, для манипуляции набором цветов. Второй интерфейс, ПЬоИп/о - пример метаинтерфейса. Все его операции служат для того, чтобы предоставить приложению способ работать с алгоритмами реобразования как с инструментами. Он не имеют никакого отношения к собственно преобразованию растровых образов. Этот интерфейс не расширят абстракцию компонента преобразования растровых образов, но предоставяет клиенту информацию о самом этом компоненте.
Метаинтерфейсы наиболее полезны, когда они реализованы для набора разных компонентов в системе компонентов. Такие метаинтерфейсы предоставляют системе унифицированный способ работы с компонентами, повышая, таким образом, степень повторной применимости кода посредством полиморфизма. Добавление метаинтерфейсов к существующим компонентам чаще всего выполняют разработчики клиентов. Используя метаинтерфейсы, такой клиент может получать компоненты из самых различных источников и работать со всеми ними единообразно.
Мы познакомились с тем, как обеспечить повторное применение компонентов при помощи включения и агрегирования. Однако при повторном применении компонентов не все так гладко. Поскольку внешний компонент - всего лишь клиент внутреннего, он может специализировать метод последнего, вызывая другие методы только перед ним и после него внешний компонент не может вставить новый вариант поведения в середину такого метода. Кроме того, поскольку метод может изменять внутреннее состояние компонента, к которому у клиента нет доступа, Вы не можете заменить всю реализацию отдельного метода интерфейса.
Например, если Вы используете некоторый интерфейс для открытия файла, для закрытия этого файла Вы обязаны использовать тот же самый интерфейс - поскольку Вам ничего не известно о реализации интерфейса или о какой-либо из его функций. Повторю, у внешнего компонента нет никакого дополнительного доступа или возможностей по сравнению с обычным клиентом. Внешний компонент обязан использовать внутренний совместимым и корректным образом, как и любой другой клиент.
Для устранения этих недостатков компонент можно спроектировать так, чтобы способствовать повторной применимости. То же самое верно и для классов C++, которые трудно, если не невозможно, расширять или специализировать, если они не спроектированы надлежащим образом. Классы C++, разработанные с учетом последующей настройки, имеют «защищенные методы, которые используются производным классом для получения информации о внутреннем состоянии базового класса. Компоненты СОМ могут использовать интерфейсы для того, чтобы облегчить свое включение или агрегирование.
Единственное реальное различие между наследованием и использованием функции обратного вызова или исходящего (outgoing) интерфейса, такого как ICustomize, состоит в том, что в последнем случае необходимо вручную подсоединить клиент к компоненту. Эту технику мы рассмотрим в гл. 13. Используя ее, необходимо быть осторожным с циклическим подсчетом ссылок. В общем случае ICustomize реализуют как часть своего собственного компонента с отдельным счетчиком ссылок.
Хорошие компоненты предоставляют своим потенциальным пользователям информацию о внутреннем состоянии и интерфейсы настройки. Понастоящему хорошие компоненты предоставляют еще и реализацию интерфейсов настройки по умолчанию. Это экономит время на реализацию интерфейса, так как клиент может агрегировать реализацию по умолчанию.
Теперь у Вас все есть. В сущности, все, что дает наследование реализации, можно заново воссоздать с помощью интерфейсов, включения и агрегирования. Ясно, что наследование реализации C++ более привычно по сравнению с решением СОМ, которое представлено на рис. 8-11. Однако наcледование реализации C++ требует доступа к исходному коду базового класса, а это означает, что при изменении базового класса программу приводится перекомпилировать. В компонентной архитектуре, где компоненты постоянно и независимо друг от друга изменяются, а исходные коды недоступны, перекомпиляция невозможна. Делая повторное применение компонентов аналогичным простому использованию, СОМ ослабляет взаимозависимость между данным компонентом и компонентом, который он специализирует.
Мы познакомились с тем, как можно повторно использовать, расширять и специализировать компоненты посредством включения и агрегирования. Повторное использование компонента столь же просто, как и включение его в другой компонент. В этом случае внешний компонент - клиент внутреннего. Если Вы хотите специализировать компонент, можете добавить свой код перед вызовом методов его интерфейсов и после него.
Если Вы собираетесь только добавлять интерфейсы, можете вместо включения использовать агрегирование. Агрегирование, как я люблю говорить, - это особый вариант включения. Когда внешний компонент агрегирует интерфейс, принадлежащий внутреннему, он передает указатель на этот интерфейс непосредственно клиенту. Внешний компонент не реализует этот интерфейс снова и не перенаправляет ему вызовы.
Компонент нельзя агрегировать, если его реализация этого не поддерживает. У внутреннего компонента должно быть два разных интерфейса IUnknown. Один из них фактически реализует методы IUnknown. Другой делегирует их вызовы неделегирующему IUnknown, если компонент не агрегирован, и IUnknown, принадлежащему внешнему компоненту, если агрегирован.
Включение и агрегирование предоставляют надежный механизм для повторного использования и настройки компонентов. Они устраняют необходимость применять в архитектурах СОМ наследование реализации. Наследование реализации нежелательно в системах, где клиенты должны быть изолированы от реализации используемых ими компонентов. Если клиент не изолирован от деталей реализации компонента, при изменении компонента его придется переписать, перекомпилировать или перекомпоновать.
В этой главе мы научились повторному применению компонентов. В следующей главе будет рассмотрен другой вариант: вместо повторного применения компонентов мы будем повторно применять код на C++. Мы реализуем базовые классы для IUnknown и IClassFactory, от которых смогут наследовать наши компоненты.
От всех этих разговоров о повторном применении у меня разыгрался аппетит. Наверное, пора узнать, насколько на самом деле съедобны миллионеры. Говорят, они хрустят на зубах.