STDMETHODIMP MyClass::GetPrimes (long nMin, long nMax, SAFEARRAY **ppsa)
{
assert(ppsa);
UINT cElems = GetNumberOfPrimes(nMin, nMax);
*ppsa = SafeArrayCreateVector(VT_I4, 0, cElems);
assert(*ppsa);
long *prgn = 0;
HRESULT hr = SafeArrayAccessData(*ppsa, (void**)&prgn);
assert(SUCCEEDED(hr));
for (UINT i=0; i < cElems; i++)
prgn[i] = GetNextPrime(i ? prgn[1 – 1] : nMin);
SafeArrayUnaccessData(*ppsa);
return S_OK;
}
Соответствующий код с клиентской стороны выглядел бы на Visual Basic примерно так:
Function GetSumOfPrimes(ByVal nMin as Long, ByVal nMax as Long) as Long
Dim arr() as Long
Dim n as Variant
Objref.GetPrimes nMin, nMax, arr
GetSumOfPrimes = 0
for each n in arr
GetSumOfPrimes = GetSumOfPrimes + n
Next n
End Function
что соответствует следующему коду на C++:
long GetSumOfPrimes (long nMin, long nMax)
{
SAFEARRAY *pArray = 0;
HRESULT hr = g_pObjRef->GetPrimes(nMin, nMax, &pArray);
assert(SUCCEEDED(hr) && SafeArrayGetDim(pArray) == 1);
long *prgn = 0;
hr = SafeArrayAccessData(pArray, (void**)&prgn);
long iUBound, iLBound, result = 0;
SafeArrayGetUBound(pArray, 1, &iUBound);
SafeArrayGetLBound(pArray, 1, &iLBound);
for (long n = iLBound; n <= iUBound: n++)
result += prgn[n];
SafeArrayUnaccessData(pArray);
SafeArrayDestroy(pArray);
return n;
}
Отметим, что вызывающая программа ответственна за освобождение ресурсов, выделенных для SAFEARRAY-массива, возвращенного как [out]-параметр. Вызов функции SafeArrayDestroy корректно освобождает всю память и все ресурсы, удерживаемые структурой SAFEARRAY.
Управление потоками данных
Отметим, что в предыдущих примерах использования массивов, в том числе типа SAFEARRAY , вопрос о том, какое количество данных будет передано в ORPC-сообщении, решал отправитель данных. Рассмотрим следующее простое определение метода на IDL:
HRESULT Sum([in] long cElems, [in, size_is(cElems)] double *prgd, [out, retval] double *pResult);
Если бы вызывающая программа должна была запустить этот метод следующим образом:
double rgd[1024 * 1024 * 16];
HRESULT hr = p->Sum(sizeof(rgd)/sizeof(*rgd), rgd);
то размер результирующего ответного сообщения ORPC-запроса был бы не меньше 128 Мбайт. Хотя лежащий в основе RPC-протокол вполне в состоянии разбивать большие сообщения на несколько сетевых пакетов, при использовании больших массивов все же возникают некоторые проблемы. Одна очевидная проблема состоит в том, что вызывающая программа должна иметь свыше 128 Мбайт доступной памяти сверх той, которая занята существующим массивом. Дублирующий буфер необходим интерфейсному заместителю для создания ответного ORPC-сообщения, в которое в конечном счете будет скопирован этот массив. Подобная проблема заключается в том, что процесс объекта также должен иметь больше 128 Мбайт доступной памяти для реконструирования полученных RPC-пакетов в единое сообщение ORPC. Если бы массив использовал атрибут [length_is], то следовало бы выделить еще 128 Мбайт, чтобы скопировать этот массив в память для передачи его методу. Эта проблема относится к параметрам как типа [in], так и [out]. В любом случае отправитель массива может иметь достаточное буферное пространство для создания OPRC-сообщения, а получатель массива – нет. Данная проблема является результатом того, что получатели не имеют механизма для управления потоками на уровне приложений.
Более сложная проблема с приведенным выше определением метода связана со временем ожидания (latency). Семантика ORPC-запроса требует, чтобы на уровне RPC/ORPC полное ORPC-сообшение реконструировалось до вызова метода объекта. Это означает, что объект не может начать обработку имеющихся данных, пока не получен последний пакет. Когда общее время передачи большого массива довольно велико, объект будет оставаться незанятым в течение значительного промежутка времени, ожидая получения последнего пакета. Возможно, что в течение этого времени ожидания многие элементы уже успешно прибыли в адресное пространство объекта; тем не менее, семантика вызова метода в СОМ требует, чтобы к началу текущего вызова присутствовали все элементы. Та же проблема возникает, когда массивы передаются как параметры с атрибутом [out], так как клиент не может начать обработку тех частичных результатов операции, которые, возможно, уже получены к этому моменту.
Для решения проблем, связанных с передачей больших массивов в качестве параметров метода, в СОМ имеется стандартная идиома разработки интерфейсов, позволяющая получателю данных явно осуществлять управление потоками элементов массива. Эта идиома основана на передаче вместо фактических массивов специального интерфейсного указателя СОМ. Этот специальный интерфейсный указатель, называемый нумератором (enumerator), позволяет извлекать элементы из отправителя со скоростью, подходящей для получателя. Чтобы применить эту идиому к приведенному выше определению метода, понадобится следующее определение интерфейса:
interface IEnumDouble : Unknown {
// pull a chunk of elements from the sender
// извлекаем порцию данных из отправителя
HRESULT Next([in] ULONG cElems, [out, size_is(cElems), length_is(*pcFetched)] double *prgElems, [out] ULONG *pcFetched);
// advance cursor past cElems elements
// переставляем курсор после элементов cElems
HRESULT Skip([in] cElems);
// reset cursor to first element
// возвращаем курсор на первый элемент
HRESULT Reset(void);
// duplicate enumerator's current cursor
// копируем текущий курсор нумератора
HRESULT Clone([out] IEnumDouble **pped);
}
Важно отметить, что интерфейс IEnum моделирует только курсор, а отнюдь не текущий массив. Имея такое определение интерфейса, исходное определение метода IDL:
HRESULT Sum([in] long cElems, [in, size_is(cElems)] double *prgd, [out, retval] double *pResult);
преобразуется следующим образом:
HRESULT Sum([in] IEnumDouble *ped, [out, retval] double *pResult);
Отметим, что подсчет элементов больше не является обязательным, так как получатель данных обнаружит конец массива, когда метод IEnumDouble::Next возвратит специальный HRESULT (S_FALSE ).
При наличии приведенного выше определения интерфейса корректной была бы следующая реализация метода:
STDMETHODIMP MyClass::Sum(IEnumDouble *ped, double *psum) {
assert(ped && psum);
*psum = 0; HRESULT hr; do {
// declare a buffer to receive some elements
// объявляем буфер для получения нескольких элементов
enum {
CHUNKSIZE = 2048 };
double rgd[CHUNKSIZE];
// ask data producer to send CHUNKSIZE elements
// просим источник данных послать CHUNKSIZE элементов
ULONG cFetched;
hr = ped->Next(CHUNKSIZE, rgd, &cFetched);
// adjust cFetched to address sloppy objects
// настраиваем cFetched на исправление некорректных объектов
if (hr == S_OK) cFetched = CHUNKSIZE;
if (SUCCEEDED(hr))
// S_OK or S_FALSE
// S_OK или S_FALSE
// consume/use received elements
// потребляем/используем полученные элементы
for (ULONG n = О; п < cFetched; n++) *psum += rgd[n];
}
while (hr == S_OK);
// S_FALSE or error terminates
// завершается по S_FALSE или по ошибке
}
Отметим, что подпрограмма Next возвратит S_OK в случае, если у отправителя имеются дополнительные данные для посылки, и S_FALSE , если пересылка закончена. Также отметим, что в данный код включена защита от некорректных реализации, которые не утруждают себя установкой переменной cFetched при возвращении S_OK (S_OK означает, что все запрошенные элементы были извлечены).
Одно из преимуществ использования идиомы IEnum состоит в том, что она позволяет отправителю откладывать генерирование элементов массива. Рассмотрим следующее определение метода на IDL: HRESULT GetPrimes([in] long nMin, [in] long nMax, [out] IEnumLong **ppe);
Разработчик объекта может создать специальный класс, который генерирует по требованию простые числа и реализует интерфейс IEnumLong:
class PrimeGenerator : public IEnumLong {
LONG m_cRef;
// СОМ reference count
// счетчик ссылок СОМ
long m_nCurrentPrime;
// the cursor
// курсор long m_nMin;
// minimum prime value
// минимальное значение простого числа
long m_nMax;
// maximum prime value
// максимальное значение простого числа
public:
PrimeGenerator(long nMin, long nMax, long nCurrentPrime) : m_cRef(0), m_nMin(nMin), m_nMax(nMax),
m_nCurrentPrime(nCurrentPrime) { }
// IUnknown methods
// методы IUnknown
STDMETHODIMP QueryInterface(REFIID riid, void **ppv);
STDHETHODIMP_(ULONG) AddRef(void);
STDMETHODIMP_(ULONG) Release(void);
// IEnumLong methods
// методы IEnumLong
STDMETHODIMP Next(ULONG, long *, ULONG *);
STDMETHODIMP Skip(ULONG);
STDMETHODIMP Reset(void);
STDMETHODIMP Clone(IEnumLong **ppe);
};
Реализация генератора Next будет просто порождать запрошенное количество простых чисел:
STDMETHODIMP PrimeGenerator::Next(ULONG cElems, long *prgElems, ULONG *pcFetched) {
// ensure that pcFetched is valid if cElems > 1
// удостоверяемся, что pcFetched легален, если cElems больше единицы
if (cElems > 1 && pcFetched == 0) return E_INVALIDARG;
// fill the buffer
// заполняем буфер
ULONG cFetched = 0;
while (cFetched < cElems && m_nCurrentPrime <= m_nMax) {
prgElems[cFetched] = GetNextPrime(m_nCurrentPrime);
m_nCurrentPrime = prgElems[cFetchcd++];
} if (pcFetched)
// some callers may pass NULL
// некоторые вызывающие программы могут передавать NULL
*pcFetched = cFetched;
return cFetched == cElems ? S_OK : S_FALSE;
}
Отметим, что даже если имеются миллионы допустимых значений, одновременно в памяти будет находиться лишь малое их число.
Методу генератора Skip нужно просто генерировать и отбрасывать запрошенное количество элементов:
STDMETHODIMP PrimeGenerator::Skip(ULONG cElems) {
ULONG cEaten = 0; while (cEaten < cElems && m_nCurrentPrime <= m_nMax) {
m_nCurrentPrime = GetNextPrime(m_nCurrentPrime);
cEaten++; }
return cEaten == cElems ? S_OK : S_FALSE;
}
Метод Reset устанавливает курсор на начальное значение:
STDMETHODIMP PrimeGenerator::Reset(void) {
m_nCurrentPrime = m_nMin;
return S_OK;
}
а метод Clone создает новый генератор простых чисел на основе минимума, максимума и текущих значений, выданных существующим генератором:
STDMETHODIMP PrimeGenerator::Clone(IEnumLong **ppe) {
assert(ppe);
*рре = new PrimeGenerator(m_nMin, m_nMax, m_nCurrent);
if (*ppe) (*ppe)->AddRef();
return S_OK;
}
При наличии реализации PrimeGenerator реализация метода GetPrimes текущим объектом становится тривиальной:
STDMETHODIMP MyClass::GetPrimes(long nМin, long nMax, IEnumLong **ppe) {
assert(ppe);
*ppe = new PrimeGenerator (nMin, nMax, nMin);
if (*ppe) (*ppe)->AddRef();
return S_OK;
}
Большая часть этой реализации находится теперь в классе PrimeGenerator, а не в классе объекта.
Динамический вызов в сравнении со статическим
До сих пор говорилось о том, что СОМ основан на клиентских программах, имеющих на этапе разработки предварительную информацию об определении интерфейса. Это достигается либо через заголовочные файлы C++ (для клиентов C++), либо через библиотеки типов (для клиентов Java и Visual Basic). В общем случае это не представляет трудностей, так как программы, написанные на этих языках, перед употреблением обычно проходят фазу какой-либо компиляции. Некоторые языки не имеют такой фазы компиляции на этапе разработки и вместо этого распространяются в исходном коде с тем, чтобы интерпретироваться во время выполнения. Вероятно, наиболее распространенными среди таких языков являются языки сценариев на базе HTML (например, Visual Basic Script, JavaScript), которые выполняются в контексте Web-броузера или Web-сервера. В обоих этих случаях текст сценариев вкладывается в его исходном виде в файл HTML, а окружающая исполняемая программа выполняет текст сценариев «на лету», по мере анализа HTML. С целью обеспечить разнообразную среду программирования эти окружения позволяют сценариям вызывать методы СОМ-объектов, которые могут создаваться в самом тексте сценария или где-нибудь еще в HTML-потоке (например, какой-либо управляющий элемент, который является также частью Web– страницы). В таких средах в настоящее время невозможно использовать библиотеки типов или другие априорные средства для снабжения машины времени выполнения (runtime engine) описанием используемых интерфейсов. Это означает, что объекты сами должны помогать интерпретатору переводить исходный текст сценариев в содержательные вызовы методов.
Для того чтобы объекты быть использованы из интерпретирующих сред типа Visual Basic Script и JavaScript, СОМ определяет интерфейс, выражающий функциональность интерпретатора. Этот интерфейс называется IDispatch и определяется следующим образом:
[object, uuid(00020400-0000-0000-C000-000000000046)] interface IDispatch : IUnknown {
// structure to model a list of named parameters
// структура для моделирования списка именованных параметров
typedef struct tagDISPPARAMS { [size_is(cArgs)] VARIANTARG * rgvarg;
[size_is(cNamedArgs)] DISPID * rgdispidNamedArgs;
UINT cArgs; UINT cNamedArgs;
} DISPPARAMS;
// can the object describe this interface?
// может ли объект описать этот интерфейс?
HRESULT GetTypeInfoCount([out] UINT * pctinfo);
// return a locale-specific description of this interface
// возвращаем специфическое для данной локализации описание этого интерфейса
HRESULT GetTypeInfo( [in] UINT itInfo,
// reserved, m.b.z.
// зарезервировано, должно равняться нулю
[in] LCID lcid,
// locale ID
// код локализации
[out] ITypeInfo ** ppTInfo);
// put it here!
// помещаем это здесь!
// resolve member/parameter names to DISPIDs
// преобразовываем имена членов/параметров в DISPID
HRESULT GetIDsOfNames( [in] REFIID riid,
// reserved, must be IID_NULL
// зарезервировано, должно равняться IID_NULL
[in, size_is(cNames)] LPOLESTR * rgszNames,
// method+params
// метод + параметры
[in] UINT cNames,
// count of names
// количество имен
[in] LCID lcid,
// locale ID
// локальный ID
[out, size_is(cNames)] DISPID * rgid
// tokens of names
// маркеры имен
);
// access member via its DISPID
// обращаемся к члену через его DISPID HRESULT Invoke(
[in] DISPID id,
// token of member
// маркер члена
[in] REFIID riid,
// reserved, must be IID_NULL
// зарезервировано, должно равняться IID_NULL
[in] LCID lcid,
// locale ID
// локальный ID
[in] WORD wFlags,
// method, propput, or propget?
// метод propput или propget?
[in,out] DISPPARAMS * pDispParams,
// logical parameters
// логические параметры
[out] VARIANT * pVarResult,
// logical result
// логический результат
[out] EXCEPINFO * pExcepInfo,
// IErrorInfo params
// параметры IErrorInfo
[out] UINT * puArgErr
// used for type errors
// использовано для ошибок типа
);
Когда машина сценариев впервые пытается обратиться к объекту, она использует QueryInterface для запроса интерфейса IDispatch этого объекта. Если объект отклоняет запрос QueryInterface, то машина сценариев этот объект использовать не может. Если же объект успешно возвращает свой интерфейс IDispatch машине сценариев, то машина будет использовать метод GetIDsOfNames этого объекта для перевода имен методов и свойств в маркеры. Эти маркеры формально называются DISPID и являются эффективно синтаксически разобранными (parsed) целыми числами, которые единственным образом идентифицируют свойство или метод. После преобразования имени метода или свойства в маркер машина сценариев потребует запуска именованного метода/свойства через метод IDispatch::Invoke данного объекта. Отметим, что поскольку IDispatch::Invoke принимает значения параметров операции в виде массива именованных типов VARIANT с использованием структуры DISPPARAMS, то диапазон поддерживаемых типов параметров ограничен возможностью записи в один VARIANT.
Интерфейсы на базе IDispatch (часто называемые dispinterface – диспинтерфейс, или диспетчерский интерфейс ) логически эквивалентны обычному интерфейсу СОМ. Основное различие состоит в методах вызова на практике логических операций интерфейса. В случае обычного интерфейса СОМ вызовы методов основываются на статике, на априорном знании сигнатуры методов интерфейса. В случае диспинтерфейса вызовы методов основаны на текстовых представлениях ожидаемой сигнатуры вызовов методов. Если вызывающая программа правильно угадывает сигнатуру метода, то вызов может быть правильно диспетчеризован. Если же вызывающая программа неправильно угадывает сигнатуру метода, то диспетчеризовать вызов, возможно, не удастся. Если для параметров метода используются неверные типы данных, то преобразование их в нужные является делом объекта (если это вообще возможно).
Простейший способ выразить диспинтерфейс на IDL – это использовать ключевое слово dispinterface:
[uuid(75DA6450-DD0F-11d0-8C58-0880C73925BA)] dispinterface DPrimeManager {
properties: [id(1), readonly] long MinPrimeOnMachine;
[id(2)] long MinPrime;
methods: [id(3)] long GetNextPrime([in] long n);
}
Этот синтаксис вполне читабелен; однако он предполагает, что вызывающая программа будет всегда обращаться к свойствам и методам объекта через IDispatch. История показала, что по мере развития программных сред этапа разработки и выполнения они часто становятся способными использовать обычные интерфейсы СОМ. Для обеспечения того, чтобы обращение к диспинтерфейсу было успешным и в будущих средах подготовки сценариев, как правило, лучше моделировать интерфейс как двойственный, или дуальный (dual interface).
Двойственные интерфейсы являются обычными интерфейсами СОМ, наследующими от IDispatch. Поскольку IDispatch является базовым интерфейсом, то он абсолютно совместим с полностью интерпретируемыми клиентами сценариев. В то же время этот интерфейс совместим вверх со средами, которые могут непосредственно связываться со статически определенным интерфейсом СОМ. Ниже приведено IDL-определение для двойственного варианта интерфейса DPrimeManager:
[object, dual, uuid(75DA6450-DD0F-11d0-8C58-0080C73925BA)] interface DIPrimeManager : IDispatch {
[id(1), propget]
HRESULT MinPrimeOnMachine( [out, retval] long *pval); [id(2), propput]
HRESULT MinPrime([in] longval);
[id(2), propget] HRESULT MinPrime([out, retval] long *pval);
[id(3)] long GetNextPrime([in] long n);
}
Заметим, что этот интерфейс наследует IDispatch, а не IUnknown. Также отметим, что данный интерфейс имеет атрибут [dual] . Этот атрибут заставляет сгенерированную библиотеку типов включить в себя диспетчерский вариант интерфейса, который совместим со средами, не поддерживающими двойственные интерфейсы. Атрибут [dual] относится к категории атрибутов [oleautomation] и также заставляет сгенерированную библиотеку типов добавлять ключи реестра для универсального маршалера во время выполнения RegisterTypeLib.
Если интерфейс определен как двойственный, то реализация методов IDispatch является тривиальной. Дело в том, что синтаксический анализатор библиотеки типов реализует два из четырех методов IDispatch . Если двойственный интерфейс был определен заранее, объекту необходимо на этапе инициализации просто загрузить библиотеку типов: class PrimeManager:
DIPrimeManager { LONG m_cRef;
// СОМ reference count
// счетчик ссылок СОМ ITypeInfo *m_pTypeInfo;
// ptr. to type desc.
// указатель на описание типов
// IUnknown methods…
// методы IUnknown…
// IDispatch methods…
// методы IDispatch…
// IPrimeManager methods…
// методы IPrimeManager…
PrimeManager(void) : m_cRef(0) {
ITypeLib *ptl = 0;
HRESULT hr = LoadRegTypeLib(LIBID_PrimeLib, 1, 0, 0, &ptl);
assert(SUCCEEDED(hr));
hr = ptl->GetTypeInfoOfGuid(IID_DIPrimeManager, &m_pTypeInfo);
ptl->Release(); } virtual PrimeManager(void) { m_pTypeInfo->Release(); }
};
Имея приведенное выше определение класса, метод GetTypeInfo просто возвращает описание данного интерфейса:
STDMETHODIMP PrimeManager::GetTypeInfo (UINT it, LCID lcid, ITypeInfo **ppti) {
assert(it == 0 && ppti != 0);
(*ppti = m_pTypeInfo)->AddRef();
return S_OK;
}
Если бы объект поддерживал несколько локализованных библиотек типов, то реализации следовало бы использовать параметр LCID, чтобы решить, какое описание типа нужно возвратить. Соответствующая реализация GetTypeInfoCount еще проще:
STDMETHODIMP PrimeManager::GetTypeInfoCount(UINT *pit) {
assert(pit != 0); *pit = 1;
// only 0 or 1 are allowed
// допускаются только 0 или 1
return S_OK;
}
Единственными допустимыми значениями счетчика являются нуль (это означает, что данный объект не содержит описаний своего интерфейса) и единица (это означает, что данный объект содержит описания своего интерфейса). Даже если объект поддерживает несколько локализованных описаний типа, результирующий счетчик остается равным единице.
Методы GetTypeInfo и GetTypeInfoCount фактически являются вспомогательными. Истинным ядром интерфейса IDispatch являются методы GetIDsOfNames и Invoke. Реализация GetIDsOfNames направляет вызов в машину синтаксического анализа библиотеки типов, встроенную в СОМ:
STDMETHODIMP PrimeManager::GetIDsOfNames(REFIID riid, OLECHAR **pNames, UINT cNames, LCID lcid, DISPID *pdispids) {
assert(riid == IID_NULL);
return m_pTypeInfo->GetIDsOfNames(pNames, cNames, pdispids);
}
Поскольку библиотека типов содержит все имена методов и соответствующие им DISPID , реализация не представляет труда для синтаксического анализатора. Метод Invoke реализован аналогичным образом:
STDMETHODIMP PrimeManager::Invoke(DISPID id, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pd, VARIANT *pVarResult, EXCEPINFO *pe, UINT *pu) {
assert(riid == IID_NULL);
void *pvThis = static_cast(this);
return m_pTypeInfo->Invoke(pvThis, id, wFlags, pd, pVarResult, pe, pu);
}
Первым параметром ITypeInfo::Invoke является указатель на интерфейс. Тип этого интерфейса должен быть таким же, как интерфейс, который описан в информации о типах. Когда передаваемые аргументы корректно синтаксически преобразованы в стек вызова (call stack), синтаксический анализатор будет вызывать текущие методы через этот интерфейсный указатель. Рис. 7.6 иллюстрирует последовательность вызовов для сред подготовки сценариев, которые осуществляют вызовы через двойственные интерфейсы.
Двунаправленные интерфейсные контракты
Как было показано в главе 5, объекты, постоянно находящиеся в различных апартаментах, могут использовать сервисные программы друг друга вне зависимости от того, резидентом какого апартамента является другой объект. Поскольку удаленный доступ в СОМ основан на концепции апартаментов, разработчикам необходимо рассматривать процессы не как клиенты или серверы в чистом виде, а скорее как набор из одного или нескольких апартаментов, которые способны одновременно экспортировать и импортировать интерфейсы.
Как два объекта договариваются о том, чьи интерфейсы будут использоваться для взаимного сотрудничества, в значительной степени является спецификой области применения. Для примера рассмотрим следующий интерфейс, моделирующий программиста:
[uuid(75DA6457-DD0F-11d0-8C58-0080C73925BA),object]
interface IProgrammer : IUnknown {
HRESULT StartHacking(void);
HRESULT IsProductDone([out, retval] BOOL *pbIsDone);
}
Клиент будет использовать такой интерфейс следующим образом:
HRESULT ShipSoftware(void)
{
IProgrammer *pp = 0;
HRESULT hr = CoGetObject(OLESTR(«programmer:Bob»), 0,
IID_IProgrammer, (void**)&pp);
if (SUCCEEDED(hr)) {
hr = pp->StartHacking();
BOOL bIsDone = FALSE;
while (!bIsDone && SUCCEEDED(hr)) {
Sleep(15000);
// wait 15 seconds
// ожидаем 15 секунд
hr = pp->IsProductDone(&bIsDone);
// check status
// проверяем состояние
}
pp->Release();
}
}
Очевидно, что этот код весьма неэффективен, поскольку клиент каждые 15 секунд опрашивает состояние объекта. Более эффективным для клиента был бы следующий подход: подготовить второй объект, которому объект-программист (programmer object) мог бы сообщить, когда данный объект придет в нужное состояние. Этот подготовленный клиентом объект должен экспортировать интерфейс, предоставляющий контекст, через который мог бы работать объект-программист:
[uuid(75DA6458-DD9F-11d0-8C58-0080C73925BA),object]
interface ISoftwareConsumer : IUnknown {
HRESULT OnProductIsDone(void);
HRESULT OnProductWillBeLate([in] hyper nMonths);
}
При таком парном определении интерфейса должен существовать некий механизм для информирования объекта-программиста о том, что у клиента имеется реализация ISoftwareConsumer, с помощью которой он может получать уведомления от объекта-программиста об изменениях состояния. Одной из распространенных методик является определение IProgrammer таким образом, чтобы он имел явные методы, через которые клиенты могли бы связываться со своими объектами-потребителями (consumer object). Канонической формой этой идиомы является включение метода Advise:
interface IProgrammer : IUnknown {
HRESULT Advise ([in] ISoftwareConsumer *psc, [out] DWORD *pdwCookie);
: : :
посредством которого клиент подготавливает парный объект-потребитель, а программист возвращает DWORD для подтверждения связи. Затем этот DWORD можно было бы использовать в соответствующем методе Unadvise:
interface IProgrammer : IUnknown {
: : :
HRESULT Unadvise([in] DWORD dwCookie);
}
для того, чтобы сообщить объекту-программисту о прерывании связи. При использовании уникальных DWORD для представления связи программист-потребитель дизайн интерфейса позволяет произвольному числу потребителей независимо друг от друга соединяться с объектом и отсоединяться от него.
Если эти два метода имеются в интерфейсе IProgrammer, то реализация программиста может быть соединена с объектом-потребителем с помощью метода Advise
STDMETHODIMP Programmer::Advise(ISoftwareConsumer *pcs, DWORD *pdwCookie)
{
assert(pcs);
if (m_pConsumer != 0) // is there already a consumer?
// уже есть потребитель?
return E_UNEXPECTED;
(m_pConsumer = pcs)->AddRef();
// hold onto new consumer
// соединяемся с новым потребителем
*pdwCookie = DWORD(pcs);
// make up a reasonable cookie
// готовим подходящий маркер
return S_OK;
}
Соответствующая реализация метода Unadvise выглядела бы примерно так:
STDMETHODIMP Programmer::Unadvise(DWORD dwCookie)
{
// does the cookie correspond to the current consumer?
// соответствует ли маркер данному потребителю?
if (DWORD(m_pConsumer) != dwCookie)
return E_UNEXPECTED;
(m_pConsumer)->Release();
// release current consumer
// освобождаем текущего потребителя
m_pConsumer = 0;
return S_OK;
}
Взаимоотношения между программистом и потребителем показаны на рис. 7.7. Хотя в данной реализации в каждый момент предусмотрен только один потребитель, возможно, что более опытный программист смог бы оперировать несколькими потребителями одновременно, управляя динамическим массивом интерфейсных указателей из ISoftwareConsumer.