Глава 8
Повторная применимость компонентов:
включение и агрегирование

Авторы статей в компьютерных журналах любят сравнивать СОМ с самыми разными вещами - например, миллионерами, горохом, C++ и компонентными архитектурами других фирм. Обычно в подобных статьях приводится какая-нибудь таблица вроде этой: Таблица 8-1. Гипотетическая, таблица из компьютерного журнала
Свойство Миллионеры Горох C++ СОМ
Съедобны x x
Поддерживают наследование N
Могут стать президентом x x x
Вы можете не обратить внимания на то, что миллионеры съедобны. Однако, читая эти статьи, Вы не сможете упустить из виду, что СОМ не поддерживает наследования. Авторы, кажется, не обращают внимания на то, что СОМ поддерживает полиморфизм - самую важную концепцию объектно-ориентированного программирования, или что СОМ - небольшая, элегантная и быстро работающая модель, или что компоненты СОМ могут прозрачно работать в сети. Их не интересует и то, что СОМ не зависит от языка программирования; что компонентов СОМ написано больше, чем компонентов какого-либо другого типа. Их интересует только одно модное словцо - наследование!

Поддерживает ли СОМ наследование? И да, и нет. То, что подразумевается в журнальных статьях - это наследование реализации, которое имеет место, когда класс наследует свой код или реализацию от базового класса. Этот вид наследования СОМ не поддерживается. Однако СОМ поддерживает наследование интерфейса, т. е. наследование классом типа или интерфейса базового класса.

Многие необоснованно утверждают, что СОМ - плохая технология, так как она не поддерживает наследование реализации. Их аргументация напоминает мне о бурных «войнах» в Интернете - OS/2 против Windows, vi против Emacs, Java против Python и т. д. Я не участвую в таких спорах, ибо это пустая трата времени.

СОМ не поддерживает наследование реализации, потому что наследование реализации слишком тесно привязывает один объект к реализации другого. Если изменится реализация базового объекта, то производные объекты не будут работать, и их тоже нужно будет изменять. Для программы среднего размера на C++ это не очень трудно - у Вас есть доступ ко всем исходным текстам, и Вы можете изменить производные классы. Однако для программ большего размера на изменение всех зависимых классов может уйти недопустимо много времени. Хуже того, у Вас может даже не быть доступа к исходным текстам. Именно поэтому эксперты по созданию больших программ на C++ настоятельно рекомендуют строить приложения на фундаменте абстрактных базовых классов.

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

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

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

Включение и агрегирование

Может быть, это национальная особенность, но, по-моему, мало кто в США доволен существующим положением дел. Мы всегда хотим все улучшить - поэтому постоянно изменяем все подряд, от прически до собственного дома. Так мы поступаем и с компонентами. После того, как кто-то дал Вам компонент, Вы наверняка захотите расширить или подстроить его под свои задачи. Кроме того, Вы можете захотеть использовать новый, усовершенствованный компонент; В C++ подстройка реализуется с помощью включения и наследования. В СОМ компоненты подстраиваются (специализируются) с помощью включения (containment) и агрегирования (aggregation).

Включение и агрегирование - это Приемы программирования, в которых один компонент использует другой. Я называю эти два компонента внешним (outer component) и внутренним (inner component) соответственно. Внешний компонент или агрегирует, или включает в себя внутренний.

Включение

Включение в СОМ похоже на включение в C++. Однако, как и все в СОМ, включение выполняется на уровне интерфейсов. Внешний компонент содержит указатели на интерфейсы внутреннего. Внешний компонент - просто клиент внутреннего компонента. Используя интерфейсы последнего, он реализует свои собственные интерфейсы (рис. 8-1).


РИС. 8-1. Внешний компонент содержит внутренний компонент и использует его интерфейс IZ.
Внешний компонент может также реализовать заново интерфейс, поддерживаемый внутренним, передавая последнему вызовы этого интерфейса. Внешний компонент может специализировать этот интерфейс, добавляя свой код перед вызовом внутреннего компонента и после этого (рис. 8-2).


РИС. 8-2. Внешний компонент содержит внутренний компонент и повторно использует его реализацию интерфейса IY.

Агрегирование

Агрегирование - это особый род включения. Когда внешний компонент агрегирует интерфейс внутреннего компонента, он не реализует интерфейс последнего заново и не передает ему вызовы этого интерфейса явно (как при включении). Вместо этого внешний компонент передает указатель на интерфейс внутреннего компонента непосредственно клиенту. Далее клиент напрямую вызывает методы интерфейса, принадлежащего внутреннему компоненту. При таком подходе внешний компонент избавлен от необходимости реализовывать заново функции интерфейса и передавать вызовы внутреннему компоненту (рис. 8-3). Однако внешний компонент не может специализировать какие-либо функции интерфейса. После того, как внешний компонент передаст интерфейс клиенту, тот обращается к внутреннему компоненту самостоятельно. Клиент не должен знать, что он работает с двумя разными компонентами, так как это нарушит инкапсуляцию. Задача агрегирования - заставить внешний и внутренний компоненты вести себя как один компонент. Как Вы увидите далее, эта возможность достигается при помощи QueryInterface.

Сравнение включения и агрегирования

Небольшой пример пояснит различия между включением и агрегированием. Предположим, что Вы хозяин небольшой металлоремонтной мастерской. У Вас есть две работницы - Памела и Анжела. Памела работает уже давно и знает свое дело досконально. Если заказ будет выполнять Памела, достаточно просто направить клиента к ней. Анжела, напротив, новичок и у нее нет опыта работы с металлом. Когда работу поручают ей, обычно нужно дать начальные указания, а иногда и сделать вместо нее наиболее сложную часть. Вы же должны и договариваться с клиентом. После того, как Анжела закончит работу, Вы вместе с ней должны проверить результаты и, может быть, что-то подправить. Случай Памелы сходен с подходом агрегирования. Дав работу, Вы уходите со сцены. Однако в случае Анжелы Вы по-прежнему ведете все переговоры с клиентом, даете начальные указания и проверяете работу в конце. Этот вариант аналогичен включению.


РИС. 8-3. Когда внешний компонент агрегирует интерфейс, он передает указатель на него непосредственно клиенту. Он не реализует интерфейс заново для передачи вызовов внутреннему компоненту.
На каждый агрегируемый Вами интерфейс будут, вероятно, приходиться сотни включаемых. Помните, всякий раз, когда компонент выступает в роли клиента и использует интерфейс, принадлежащий другому компоненту, первый, в некотором смысле, включает второй. С другой стороны, агрегирование - гораздо более специальный случай. Его используют тогда, когда некоторый компонент уже реализует некоторый интерфейс именно так, как это нужно Вам, и Вы передаете интерфейс компонента клиенту.

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

Поскольку агрегирование - это особый случай включения, мы рассмотрим включение первым.

Реализация включения

Включение компонента выполняется столь же просто, как и использование. Каталог \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 -> связано с большими ограничениями, чем агрегирование СОМ. Вы можете передавать вызовы только одному классу, тогда как в СОМ можно агрегировать сколько угодно интерфейсов.

Магия QueryInterface

Вот объявление внешнего компонента, который реализует интерфейс IX и предоставляет интерфейс IY посредством агрегирования.
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(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 ;
}
В этом примере QueryInterface внешнего компонента просто вызывает QueryInterface внутреннего. Все очень хорошо и просто, но если бы это еще правильно работало!

Проблема заключается не в приведенном коде - она в интерфейсе IUnknown внутреннего компонента. Агрегированный внутренний компонент должен обрабатывать вызовы функций членов QueryInterface особым образом. Как мы увидим, здесь фактически необходимы две реализации IUnknown.

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

Неверный IUnknown

Задача агрегирования - убедить клиента, что интерфейс, реализованный внутренним компонентом, реализован внешним. Вы должны напрямую предать клиенту указатель на интерфейс внутреннего компонента и внушить ему, что этот указатель принадлежит внешнему компоненту. Если клиенту передать указатель интерфейса, реализованного внутренним компонентом как обычно, то компонент будет представлен клиенту в расщепленном виде. Интерфейс внутреннего компонента использует реализацию QueryInterface внутреннего компонента, тогда как у внешнего компонента имеется своя собственная QueryInterface. Когда клиент прямо запрашивает интерфейсы внутреннего компонента, у него создается иное представление о возможностях компонента, чем если он запрашивает интерфейс у внешнего компонента (рис. 8-4). Сказанное можно пояснить примером. Предположим, Вы агрегировали компонент. Внешний компонент поддерживает интерфейсы IX и IY. Он реализует IX и агрегирует IY. Внутренний компонент реализует интерфейсы IY и IZ. Создав внешний компонент, мы получаем указатель на его интерфейс IUnknown. С помощью этого интерфейса можно успешно запросить интерфейс IX или IY, но запрос IZ будет возвращать E_NOINTERFACE. Если мы запросим указатель на IY, то получим указатель на интерфейс внутреннего компонента. Если запросить IZ через этот указатель на IY, то запрос будет успешным. Так получается из-за того, что функции интерфейса IUnknown для интерфейса IY реализованы внутренним компонентом. Точно так же, запрос у интерфейса IY интерфейса IX потерпит неудачу, поскольку внутренний компонент не поддерживает IX. Такая ситуация нарушает фундаментальное правило реализации QueryInterface: если Вы можете попасть в определенное место откуда-нибудь, то туда можно попасть откуда угодно.


РИС. 8-4. Разные компоненты имеют разные реализации IUnknown.
Виноват здесь интерфейс IUnknown внутреннего компонента. Клиент видит два разных IUnknown, внутренний и внешний. Это сбивает его с толку каждый из IUnknown реализует QueryInterface по-своему, и каждая QueryInterface поддерживает разные наборы интерфейсов. Клиент должен быть абсолютно независим от реализации компонента-агрегата. Он не должен знать, что внешний компонент агрегирует внутренний, и никогда не должен видеть IUnknown внутреннего компонента. Как было сказано в гл. 3, два интерфейса реализованы одним и тем же компонентом тогда и только тогда, когда оба они возвращают один и тот же указатель в ответ на запрос указателя На IUnknown. Следовательно, необходимо дать клиенту единственный IUnknown и скрыть от него IUnknown внутреннего компонента. Интерфейсы внутреннего компонента должны использовать интерфейс IUnknown, реализованный внешним компонентом. IUnknown внешнего компонента называют внешним IUnknown (outer unknown), или управляющим IUnknown (controlling unknown).

Интерфейсы IUnknown для агрегирования

Самым простой для внутреннего компонента способ использовать внешний IUnknown - передавать ему вызовы своего IUnknown. Для этого внутренний компонент должен знать, что он агрегируется, и должен иметь указатель на внешний IUnknown.
Внешний IUnknown
Из гл. 7 Вы помните, что функциям CoCreateInstance и IClassFactory::CreateInstance передается указатель на 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.
Делегирующий и неделегирующий IUnknown
Для поддержки агрегирования внутренний компонент фактически реализует два интерфейса IUnknown. Неделегирующий (nondelegating) IUnknoun реализует IUnknown внутреннего компонента обычным образом. Делегирующий (delegating) IUnknown передает вызовы методов IUnknown либо внешнему IUnknown, либо неделегирующему IUnknown. Если внутренний компонент агрегируется, делегирующий IUnknown передает вызовы внешнему IUnknown, реализованному внешним компонентом. Клиенты агрегата вызывают делегирующий IUnknown, тогда как внешний компонент работает с внутренним через неделегирующий. Эту ситуацию поясняют рисунки: вариант без агрегирования показан на рис. 8-5, вариант с агрегированием - на рис. 8-6.

Из рис. 8-5 видно, что часть интерфейса IY, относящаяся к IUnknown, переправляет вызовы делегирующему IUnknown. Делегирующий IUnknown вызывает неделегирующий. Последний в действительности и реализует интерфейс IUnknown обычным образом.


РИС. 8-5. Если компонент не агрегируется, его делегирующий IUnknown передает вызовы неделегирующему lUnknoun.
На рис. 8-6 представлен компонент, агрегирующий IV. В этом случае делегирующий IUnknown вызывает IUnknoun, реализованный внешним компонентом. Внешний компонент вызывает неделегирующий IUnknown для управления временем существования внутреннего компонента. Таким образом, когда некоторый компонент вызывает метод IUnknown через указатель на интерфейс IY, он вызывает делегирующий IUnknown, который перенаправляет вызов внешнему IUnknown. В результате внутренний компонент использует реализацию IUnknown внешнего компонента.


РИС. 8-6. Если компонент агрегирован, его делегирующий IUnknown передает вызовы внешнему IUnknoun.
Теперь все, что мы должны сделать, - это реализовать делегирующий и неделегирующий IUnknown.
Реализация делегирующего и неделегирующего IUnknown
Нам необходимо иметь для компонента две разные реализации IUnknown. Но C++ не позволяет реализовать интерфейс дважды в одном классе. Следовательно, мы изменим имя одного из IUnknown, чтобы избежать конфликта имен. Я выбрал имя INondelegatingUnknown. Вы можете выбрать то, которое Вам больше нравится. Вспомните, что для СОМ имена интерфейсов не имеют значения; СОМ интересуется только структурой vtbl. INondelegatingUnknown объявлен так же, как и IUnknown, за исключением того, что названия всех функций-членов имеют префикс «Nondelegating».
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);
	}
	else if (iid == IID_IY)
	{
		*ppv = static_cast(tnis) ;
	}
	else
	{
		*ppv = NULL ;
		return E_NOINTERFACE ;
	}
	reinterpret_cast(*ppv)->AddRef() ;
	return S_OK ;
}
Обратите внимание на приведение типа указателя this внутреннего компонента к INondelegatingUnknown. Это приведение очень важно. Преобразуя this к INondelegatingUnknown, мы гарантируем, что будет возвращен неделегирующий IUnknown. Неделегирующий IUnknown всегда возвращает указатель на себя, если у него запрашивается IID_IUnknown. Без этого приведения типа вместо неделегирующего IUnknown возвращался бы делегирующий. Когда компонент агрегируется, делегирующий IUnknown передает все вызовы QueryInterface, Release и AddRef внешнему объекту.

Клиенты агрегируемого компонента никогда не получают указатели на неделегирующий IUnknown внутреннего компонента. Всякий раз, когда клиент запрашивает указатель на IUnknown, он получает IUnknown внешнего компонента. Указатель на неделегирующий IUnknown внутреннего компонента передается только внешнему компоненту. Теперь рассмотрим, как реализовать делегирующий IUnknown.

Pеализация делегирующего IUnknown
К счастью, реализация делегирующего IUnknown проста - она передает вызовы либо внешнему, либо неделегирующему IUnknown. Далее приводится объявление компонента, поддерживающего агрегирование. Компонент содержит указатель m_pUnknownOuter. Если компонент агрегирован, указатель ссылается на внешний IUnknown. Если компонент не агрегирован, этот указатель ссылается на неделегирующий IUnknown. Всякий раз при обращении к делегирующему IUnknown вызов переадресуется интерфейсу, на который указывает m_pUnknownOuter. Делегирующий IUnknown реализован функциями, подставляемыми в строку (inline):
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 ;
};

Создание внутреннего компонента

Теперь, когда мы знаем, как реализовать внутренний компонент, обсудим, как он создается внешним компонентом. Чтобы пройти весь процесс создания, от начала до конца, рассмотрим код трех функций: функция Init внешнего компонента начинает процесс; затем вступают функция фабрики класса CreateInstance и конструктор внутреннего компонента.
Функция Init внешнего компонента
Первое, что делает внешний компонент при агрегировании - создает внутренний компонент. Основное различие между включением и агрегированием состоит в том, .что во втором случае внешний компонент передает внутреннему внешний IUnknown. Приведенный ниже фрагмент кода показывает, как внешний компонент создает внутренний. Обратите внимание, что второй параметр CoCreateInstance - это указатель на интерфейс IUnknown внешнего компонента.

Кроме того, отметьте себе, что пятый параметр запрашивает у внутреннего компонента указатель на 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 внешнего компонента остается неизменной. Фабрика же класса внутреннего компонента подверглась некоторым изменениям, которые мы теперь и рассмотрим.
Функция IClassFactory::CreateInstance внутреннего компонента
Реализация IClassFactory::CreateInstance внутреннего компонента изменена чтобы использовать INondelegatingUnknown вместо IUnknown. Код этой функции приведен ниже, отличия от предыдущих вариантов CreateInstance.выделены полужирным. Обратите внимание, что ClassFactory::CreateInstance не возвращает автоматически ошибку, если pUnknownOuter не равен NULL (т. е. когда внешний компонент желает агрегировать внутренний). Однако CreateInstance обязана возвратить ошибку, если при этом iid отличен от IID_IUnknown. Когда компонент создается как внутренний в агрегате, он может возвратить только интерфейс IUnknown, иначе внешний компонент никогда не получил бы неделегирующего IUnknown (поскольку вызов QueryInterface будут делегированы внешнему IUnknown).
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.
Конструктор внутреннего компонента
В приведенном выше коде CreateInstance указатель на внешний IUnknown передается конструктору внутреннего компонента. Конструктор инициализирует m_pUnknownOuter, которая используется делегирующим IUnknown для передачи вызовов либо неделегирующему, либо внешнему IUnknown. Если компонент не агрегируется (pUnknownOuter есть NULL), конструктор помещает в m_pUnknownOuter указатель на неделегирующий IUnknown. Это показано в приведенном ниже фрагменте:
СВ::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 ;
}
}

Указатели внешнего компонента на интерфейсы внутреннего компонента

Когда я реализовывал CA::Init при создании внутреннего компонента, запрашивал интерфейс IUnknown, а не IY. Однако наш компонент в девствительности агрегирует IY. Поэтому неплохо было бы в самом начал проверить, поддерживает ли интерфейс IY внутренний компонент. Но, как указывалось выше, при агрегировании компонента внешний компонен может запрашивать только интерфейс IUnknown. CFactory::Createlnstance возвращает CLASS_E_NOAGGREGATION, если ей передано что-либо, отличное от IID_IUnknown. Следовательно, нам необходимо запросить у внутренего компонента интерфейс IY после его (компонента) создания.

Но здесь необходимо быть аккуратным и не запутаться. Когда Вы вызывате 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 ;
}

Слепое агрегирование

В предыдущем примере внешний компонент агрегирует только один из реализуемых внутренним компонентом интерфейсов. Единственный интерфейс внутреннего компонента, до которого может добраться клиент, это IY. Если бы внутренний компонент реализовывал IZ, клиент не смог бы получить указатель на IZ, так как внешний компонент возвращал бы E_NOINTERFACE.

А что, если внешний компонент хочет агрегировать несколько интерфейсов внутреннего? Внешний компонент легко модифицировать так, чтобы поддерживать еще один интерфейс внутреннего компонента:

else if ((lid == IID_IY) || (iid == IID_IZ))
{
 return m_pIUnknownInner->QueryInterface(iid, ppv) ;
}
Конечно, для изменения кода внешнего компонента нам потребуется доступ к этому коду и компилятор. Но как быть, если мы хотим, чтобы клиент имел доступ ко всем интерфейсам внутреннего компонента, включая те, что будут добавлены после того, как внешний компонент будет написан, скомпилирован и поставлен заказчику? Все очень просто - удалите условие. Вместо проверки идентификатора интерфейса можно просто слепо передавать его внутреннему компоненту:
else if (iid == IID_IX)
{
*ppv = static_cast(this) ;
}
else // Нет условия
{
return m_pllnknownInner->QueryInterface(iid, ppv) ;
}
Данная процедура называется слепым агрегированием (blind aggregation), так как внешний компонент слепо передает идентификаторы интерфейсов внутреннему. При слепом агрегировании внешний компонент не контролирует, какие из интерфейсов внутреннего компонента он предоставляет клиенту. В большинстве случаев лучше не прибегать к слепому агрегированию. Одна из причин этого в том, что внутренний компонент может поддерживать интерфейсы, не совместимые с интерфейсами, которые поддерживаются внешним компонентом. Например, внешний компонент для сохранения файлов может поддерживать интерфейс ISlowFile, тогда как внутренний для этой же цели поддерживает интерфейс IFastFile. Предположим, что клиент всегда запрашивает IFastFile прежде, чем запрашивать ISlowFile. Если внешний компонент слепо агрегирует внутренний, клиент получит указатель на IFastFile внутреннего компонента. Внутренний компонент ничего не знает о внешнем и поэтому, не будет правильно сохранять информацию, связанную с последним. Проще всего избежать таких конфликтов, не используя слепое агрегирование.

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

Метаинтерфейсы
Интерфейсы, вызывающие конфликты внутреннего и внешнего компонента, - это обычно интерфейсы с перекрывающейся функциональностью. Если Вы можете гарантировать, что функциональность интерфейсов не перекрывается, вероятность конфликтов уменьшается. Добиться этого непросто, так как внутренний компонент может быть модернизирован для поддержки новых интерфейсов, которых внешний компонент не ожидает. Вероятность конфликта с существующими интерфейсами компонента минимальна в случае метаинтерфейсов (metainterfaces), или интерфейсов класса. Метаинтерфейсы манипулируют самим компонентом, а не абстракцией, которую он представляет.

Предположим, что у нас есть программа преобразования растровых изображений. Пользователь может модифицировать растровое изображение c помощью различных алгоритмов. Последние реализованы как внутренний компонент, который пользователь может добавлять в систему во время работы. Каждый внутренний компонент может считывать и сохранять растровые образы, а также преобразовывать их в соответствии с некоторым особым алгоритмом. У внешнего компонента есть интерфейс ISetColors, устанавливающий цвета, с которыми работает внутренний компонент. Внешний компонент также имеет интерфейс IToolInfo, который отображает значки разных алгоритмов преобразования на панели инструментов и создает внутренний компонент, когда пользователь выбирает соответствующий значок (рис. 8-7). ISetColors - это пример обычного интерфейса, который расширяет абстракцию алгоритма преобразования. Компонент преобразования, вероятно, уже поддерживает интерфейс, например IColors, для манипуляции набором цветов. Второй интерфейс, ПЬоИп/о - пример метаинтерфейса. Все его операции служат для того, чтобы предоставить приложению способ работать с алгоритмами реобразования как с инструментами. Он не имеют никакого отношения к собственно преобразованию растровых образов. Этот интерфейс не расширят абстракцию компонента преобразования растровых образов, но предоставяет клиенту информацию о самом этом компоненте.

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


РИС. 8-7. Интерфейс IToolInfo - это метаинтерфейс, вероятность конфликта которого с интерфейсами внутреннего компонента мала.
SetColors не является метаинтерфейсом и действует в области компетенции внутреннего компонента. ISetColors конфликтует с методом IColors внутреннего компонента.
Взаимосвязанные пары
Другой способ избежать конфликтов интерфейсов - дать клиенту дополнительные сведения о внешнем компоненте. Если клиент и внешний компонент разрабатывались совместно, первый знает, какие интерфейсы реализованы вторым, и может использовать их вместо тех, что потенциально могут реализоваться внутренним компонентом. Если совместно разрабатывались внешний и внутренний компоненты, их можно спроектировать так, чтобы конфликтных интерфейсов не было. Однако подобные взаимосвязанные пары требуют, чтобы Вы .контролировали не только внешний компонент, Но также и клиент либо внутренний компонент.

Агрегирование и включение в реальном мире

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

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

Для устранения этих недостатков компонент можно спроектировать так, чтобы способствовать повторной применимости. То же самое верно и для классов C++, которые трудно, если не невозможно, расширять или специализировать, если они не спроектированы надлежащим образом. Классы C++, разработанные с учетом последующей настройки, имеют «защищенные методы, которые используются производным классом для получения информации о внутреннем состоянии базового класса. Компоненты СОМ могут использовать интерфейсы для того, чтобы облегчить свое включение или агрегирование.

Предоставление информации о внутреннем состоянии

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


РИС. 8-8. Внутренний компонент может упростить свою настройку, предоставив интерфейсы, которые дают внешнему компоненту доступ к его внутреннему состоянию.
Интерфейсы, предоставляющие внешнему компоненту такого рода информацию, играют в СОМ важную роль. Компоненты СОМ обычно состоят из множества небольших интерфейсов. Внутри себя реализации этих интерфейсов взаимозависимы, так как используют общие переменные-члены и другие аспекты внутреннего состояния. Это означает, что Вы не можете взять один из интерфейсов компонента и использовать его сам по себе, потому что этот интерфейс может зависеть (в управлении некоторым аспектом внутреннего состояния компонента) от информации или состояния другого интерфейса (рис. 8-9). Повторю еще раз, внешний компонент - просто клиент внутреннего. У него нет никаких особых возможностей.


РИС. 8-9. Клиент рассматривает интерфейсы как независимые сущности. Однако обычно интерфейсы используют общие детали реализации. Это еще более затрудняет расширение или специализацию компонентов. Добавление интерфейсов, предоставляющих взгляд на компонент изнутри, может упростить его настройку.

Моделирование виртуальных функций

Дополнительные интерфейсы могут не только предоставить эквивалент СОМ для защищенных функций-членов C++, но и позволить интерфейсам замещать виртуальные функции. Во многих случаях виртуальные функции используются как функции обратного вызова (callback). Базовый класс может вызывать виртуальную функцию до, во время или после некоторой операции, чтобы дать производному классу возможность модифицировать ее выполнение. Компоненты СОМ могут делать то же самое, определив интерфейс настройки (customization interface). Компонент не реализует такой интерфейс, а, наоборот, вызывает его. Клиенты, желающие настроить компонент для своих нужд, реализуют интерфейс настройки и передают указатель на него компоненту. Эту технику клиенты могут применять, и не используя включение или агрегирование (рис. 8-10).


РИС. 8-10. Компонент определяет исходящий интерфейс, который он вызывает для своей настройки.

Единственное реальное различие между наследованием и использованием функции обратного вызова или исходящего (outgoing) интерфейса, такого как ICustomize, состоит в том, что в последнем случае необходимо вручную подсоединить клиент к компоненту. Эту технику мы рассмотрим в гл. 13. Используя ее, необходимо быть осторожным с циклическим подсчетом ссылок. В общем случае ICustomize реализуют как часть своего собственного компонента с отдельным счетчиком ссылок.

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

Теперь у Вас все есть. В сущности, все, что дает наследование реализации, можно заново воссоздать с помощью интерфейсов, включения и агрегирования. Ясно, что наследование реализации C++ более привычно по сравнению с решением СОМ, которое представлено на рис. 8-11. Однако наcледование реализации C++ требует доступа к исходному коду базового класса, а это означает, что при изменении базового класса программу приводится перекомпилировать. В компонентной архитектуре, где компоненты постоянно и независимо друг от друга изменяются, а исходные коды недоступны, перекомпиляция невозможна. Делая повторное применение компонентов аналогичным простому использованию, СОМ ослабляет взаимозависимость между данным компонентом и компонентом, который он специализирует.


РИС. 8-11. Компонент предоставляет реализацию исходящего интерфейса по умолчанию.

Резюме

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

Если Вы собираетесь только добавлять интерфейсы, можете вместо включения использовать агрегирование. Агрегирование, как я люблю говорить, - это особый вариант включения. Когда внешний компонент агрегирует интерфейс, принадлежащий внутреннему, он передает указатель на этот интерфейс непосредственно клиенту. Внешний компонент не реализует этот интерфейс снова и не перенаправляет ему вызовы.

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

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

В этой главе мы научились повторному применению компонентов. В следующей главе будет рассмотрен другой вариант: вместо повторного применения компонентов мы будем повторно применять код на C++. Мы реализуем базовые классы для IUnknown и IClassFactory, от которых смогут наследовать наши компоненты.

От всех этих разговоров о повторном применении у меня разыгрался аппетит. Наверное, пора узнать, насколько на самом деле съедобны миллионеры. Говорят, они хрустят на зубах.

[Назад] [Содержание] [Вперед]




Смотрите также:



Hosted by uCoz