Предыдущая Оглавление Следующая

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

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

  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!");
  return 0; 
}

с такой же, но написанной для 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:
    ...
  }
  return DefWndProc(...); 
}

и приходил к выводу, что "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. Именно это - как сконструировать декомпозицию на модули, а внутри модулей - декомпозицию на объекты и составляет главную трудность компонентного программирования. А - совсем не инструмент, который используется при кодировании... Но - всему своё время. Это - лишь небольшое замечание философской природы, пришедшееся к месту в общем потоке изложения.

Предыдущая Оглавление Следующая