Скачиваний:
100
Добавлен:
01.05.2014
Размер:
1.56 Mб
Скачать

Еще один пример

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

  1. он делал объекты, но не позволял их уничтожать, когда надобность в них исчезает;

  2. он не позволял на стороне клиента "знать" что же за указатель мы получили.

И если объект реализовывал более одного интерфейса, то это обстоятельство здорово сковывало свободу клиента - в "обычном попроектном программировании" клиент мог с указателем делать значительно больше.

Исправим пример. Ход исправления - сделаем наш интерфейс NeoInterfaceнаследником системно определенного интерфейсаIUnknown. Напишем реализацию методов интрефейсаIUnknownв составе наших объектов, а реализацию "полезных" методов трогать не будем - мы только их оформим "как положено" - с возвратом типаHRESULT.

Мы не будем трогать и конструкцию COM-сервера, удовлетворимся на этот раз тем, что "тот" сервер работал.

Мы перепишем клиент: наша задача в данный раз - показать как функционирует объект "под управлением IUnknown", а не то, что концептуальная схема "вообще работоспособна". Поэтому мы изменим окно главного диалога - оно теперь не будет вызывать методы двух объектовCBanzaiиCHello, а будет порождать отдельные окна - графические объекты. Эти графические объекты будут относительно самостоятельны - при их создании "родительское окно" снабдит их указателем на интерфейс соответствующего объекта, и уже "маленькие окна" будут вызывать методы объектов сервера. Маленьких окон можно породить много - они немодальны, поэтому методы объектов сервера, сцепленных с окном, можно вызывать в произвольном порядке и наблюдать за их поведением в разных ситуациях. Обратите внимание - в данном случае будут вызываться методы уженастоящихCOM-объектов.

Исходные проекты этого примера находятся здесь. С ними нужно поступить так же, как и с проектами предыдущего примера - собрать, и скопировать исполняемые модули в один каталог.

Для тестирования запустите EXE. Программа покажет диалог с тремя кнопками - "Создать Banzai", "Создать Hello", "Закрыть панель". Кнопка "Закрыть панель" завершает весь процесс. Кнопки "Создать..." создают новое "маленькое окно" и получают у серверановый экземпляр указателя на COM-интерфейс. В составе "маленького окна" имеются кнопки вызова соответствующих методов -Show,Sound,Releaseи кнопка "Клонировать". Кнопка "Клонировать" "размножает диалог", т.е. она создаёт точно такое же окно - сёстринское по отношению к себе - и наделяет егокопией существующего указателя на COM-интерфейс. Продвигая при этом счётчик ссылок.

Объекты CBanzaiиCHelloостались теми же -CBanzaiостался статическим объектом сервера, аCHello- получается в динамической памяти. Поэтому "управлять временем жизни" имеет и смысл и видимый результат только для объектаCHello. Это не запрещено и дляCBanzai, но вы увидите разницу сами.

Как следствие, вы можете получить объект и клонировать его, скажем, раза два... Три раза (при получении и дважды при клонировании) продвинется счётчик ссылок. А потом вы в произвольном порядке можете вызывать Releaseиз всех этих "маленьких окон" - объект уничтожится только после третьего вызоваRelease. Операцииnew/deleteв составе сервера переопределены на собственную реализацию. Она при вызовеdeleteвыдаёт на экран сообщение "освобождается блок памяти", т.е. вы воочию увидите когда именно объект уничтожается - когда срабатываетdelete this. "Маленькое окно" закрывается кнопкой системного меню (крестик), а не кнопкойRelease- это сделано для того, чтобы вы могли попробовать получить три копии объекта, а вызвать методReleaseна один раз больше... У нас - небольшие модули, поэтому они и обрушиваются тоже быстро и без побочных последствий.

Во-первых, мы написали очень примитивный, но настоящий COM-объект. И есть смысл, пока содержательные особенности не заслонили просто реализации протокола, сравнитьCOM-объект с обычным объектом. Точнее, сравнить, что стОит для программиста писать вCOM-технологии относительно технологии обыкновенной, с точки зрения его "ручного труда".

У нас есть "обычный класс" из "обычного проекта". Каковы затраты, чтобы преобразовать его формат к COM? Оба примера дают на это совершенно очевидный ответ: 1) нужно разработать абстрактный класс, который бы описывал методы экспонируемые наружу; 2) нужно реализоватьIUnknown; 3) в составе сервера нужно реализовать аналог "собственногоnew" -DllGetClassObject.

Обратите внимание - эти затраты почти постоянны и слабо зависят от сложности объекта с содержательной его стороны. Сколь бы ни был сложен объект, реализация IUnknownу него только одна. Иерархию же абстрактных типов и так рекомендуется разрабатывать уже для средних по размеру "обычных" проектов, чтобы иметь достаточно жёсткий архитектурный каркас. И - это уже из собственного опыта - я редко встречал даже средние приемлемо работающие проекты, где программист бы не покушался на переопределениеnew/deleteхотя бы для одного класса из проекта. Т.е. "чистых затрат" кодирования при написании программы "вCOM" относительно "обычного проекта", в общем-то только - реализацияIUnknownдля каждогоCOM-объекта, да в клиенте - постоянный вызовAddRef/Releaseсопровождающий экзистенцию указателя...

Кроме того, как мы когда-то увидим, вовсе не каждый класс C++в проекте является статическим типомCOM, а наоборот - некоторая совокупность классовC++реализует одинCOM-объект. Поэтому причина, которая заставляет думать, что "COM- это очень сложно" только одна - нетрадиционная (на данном историческом этапе развития программирования, со временем ситуация изменится на обратную), сравнительно с учебником классического программирования, архитектура. Наверное, играет какую-то роль и то, что вCOMкак-то уж очень неудобно писать школярские программы наподобие "Hello, world!" - "полезная нагрузка" такой программы явно диссонирует с количеством кода, которое нужно написать для соблюдения просто протокола.

Это - не уникальное явление. Оно уже наблюдалось, когда (по современным меркам - давным-давно) народ сравнивал простую и ясную программу для MS DOS:

int main()

{

printf("Hello, world!");

return0;

}

с такой же, но написанной для MS Windows:

int WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPWSTR lpCmdLine, int nShowCmd)

{

...

RegisterClass(...);

...

CreateWindow(...);

...

}

LONG FAR PASCAL MainWndProc(HWND hWnd, unsigned message, WORD wParam, LONG lParam)

{

switch(message)

{

...

case WM_PAINT:

...

}

returnDefWndProc(...);

}

и приходил к выводу, что "DOS- намного лучше и эффективнее...". Для "Hello, world!" - безусловно, для значительно же более сложных и развитых программ правильный ответ оказался немного не столь очевидным...

Вторым обстоятельством заслуживающим внимания является то, что собственно "операции поддержки протокола COM" концентрируются только в двух местах - в реализацииQueryInterfaceи в реализацииDllGetClassObject. Причём в обоих случаях код получается чисто механическим - при реализации объектом ещё одного интерфейса вQueryInterfaceдобавятся строки сравненияIIDи операции приведения типа указателя к данному типу. А если сервер будет реализовывать ещё и другие статические типы, то аналогичные строки добавятся вDllGetClassObject. Это позволяет "высокомеханизировать" написание каркасовCOM-объектов. Что, например, и сделано вMFCпосредством wizard, а вATL- посредством соответствующих шаблонов. Также и автоматическое оформление полнофункциональныхCOM-объектов вPascalили вVisual Basicсамим компилятором опирается на данное обстоятельство, что позволяет программисту концентрироваться на том, на чём и положено - на содержательной частиCOM-объекта, а не на реализации служебного кода протокола. Добавление же методов в "полезный интерфейс" объекта вообще не требует никаких действий в его "каркасе", как это видно из нашего примера.

Третьим важным обстоятельством является то, что уже и просто "формат COM", сама возможность писать программы в такой "автономной архитектуре", влияет на конструкцию совокупного программного комплекса и - в лучшую сторону. Давайте попробуем произвести обратное преобразование наших простых классовCBanzaiиCHelloиз "компонентной архитектуры" в "обычную". Оно тривиально - нужно удалить реализацииIUnknownиDllGetClassObjectи слить проекты клиента и сервера, заменив вызовCoCreateInstanceна вызовnew. Попробуйте это сделать в действительности - это и на самом деле несложно.

Обнаружили "изюмину"? А как же! В клиенте нужно всё время возиться с указателем на объект стремясь его не потерять и где-то (где?) не забыть вызвать к нему delete... и ровно один раз. Мелочь, конечно, но как возможность просто "оставить в покое" указатель в любом месте программы повышает и ясность и, главное, надёжность сложного клиента, когда существует длинная, а то и ветвящаяся последовательность передачи указателя вглубь процедур и от объекта к объекту! Я думаю, счётчик ссылок заслуживает того, чтобы это архитектурное решение применялось и в "обычных" проектах - уж больно оно удачно.

Поэтому, внимательно рассматривая наш пример, можно заподозрить, что основная трудность "компонентной архитектуры" кроется не в дополнительном служебном коде, не в том, что этого кода много или он мало понятен, а - в голове программиста. Он должен иметь соответствующее направление взгляда, видеть "компонентно". Моё собственное убеждение - кодировать наC++COM-сервер или клиент и без примененияMFCиATLстоль же (не)трудно, как и писать на нём оконные приложения без примененияMFC. Главное - знать, что писать! Производительность труда программиста в данном случае падает только на очень небольших проектах, где действительно кодирование оформления взаимодействия занимает относительно бОльшую часть кода, а собственно "содержательный код" - меньшую. В больших проектах на первый план выходят проблемы архитектуры и необходимость реализации "чего-то" нестандартно, не так, как это принято в реализации средства, в нашем случае -MFCилиATL. Но ведь то же случается и вообще в любом большом проекте и безотносительно к средству, методологии и архитектуре разработки? Хоть это и требует значительно большей квалификации программиста, ещё более дорогим решением окажется, если "системным архитектором" вашего проекта выступит wizard отVisual Studio. Именно это - как сконструировать декомпозицию на модули, а внутри модулей - декомпозицию на объекты и составляет главную трудность компонентного программирования. А - совсем не инструмент, который используется при кодировании... Но - всему своё время. Это - лишь небольшое замечание философской природы, пришедшееся к месту в общем потоке изложения.