developing.ru - клуб программистов Здесь может быть и ваша реклама.
developing.ru >технология COM >
Михаил Безверхов
aspid@developing.ru

Клиенту - клиентово, а серверу - серверово

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

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

Как избавиться от этого недостатка? Давайте подумаем. И подумаем не так, как принято в стандартном "по-проектном мышлении", а - именно с позиций подхода "клиент-сервер".

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

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

Нет! В C++ статический тип исчерпывающе описывается классом. А абстрактный класс - "не тот тип", иначе его бы не отказался использовать и сам оператор new. А new - отказывается. Поэтому для "функции сервера, умеющей создавать объекты" абстрактный класс будет всё так же неприменим, как и для "функции клиента".

Но и это - одолимая беда. Сервер же не реализует бесконечное множество статических типов объектов? Перенумеровать их - и всего-то. И запросы ставить к нему - "сделать объект типа номер один", "сделать объект типа номер три"… Только здесь возникает вот какое обстоятельство - чтобы корректно использовать "перенумерование" мы, фактически, должны использовать пары "статический тип на стороне сервера - абстрактный тип на стороне клиента" и перенумеровывать уже пары. А это - еще хуже, поскольку малейший сбой в соответствии частей останется незамеченным никем, но в большом проекте приведет к тому, что будет непонятно даже где ошибку и искать... Тупик?

Пройдемся ещё раз - по шагам. Мы не можем создать объект даже "внутри сервера" потому, что "внутри клиента" не знаем описания его статического типа ... "внутри сервера". Это мы так захотели - мы не хотим в клиенте об объекте сервера знать больше, чем тот абстрактный тип, который позволяет с этим объектом взаимодействовать. Меньше знаешь - реже перекомпилируешься. И отказываться от этого требования мы не собираемся. Но, с другой стороны, сервер-то как раз знает и полное описание статического типа, и тот абстрактный тип, на базе которого этот статический тип построен. Почему бы нам не отправлять на сервер запросы примерно такого вида - "создать объект статического типа номер четыре, построенный на базе абстрактного типа XXX"? У сервера эта информация есть. Пусть не мы на клиентской стороне, но сам сервер отслеживает соответствует ли номер статического типа абстрактному типу и может ли он такой объект создать. Поэтому, похоже, выход находится здесь - на стороне клиента нумеровать (именовать не описанием классов) статические типы и отправлять на сервер запросы из пар "номер объекта - абстрактный тип". Когда сервер "сделает из номера" настоящий объект и передаст указатель на него клиенту, то клиенту  будет уже всё равно, какой там у этого статического типа был номер, он будет применять при доступе по этому указателю исключительно абстрактный тип, который он заказывал. Эврика? Эврика...

Можно, конечно, и пофилософствовать на эту тему. Но, по-моему, всё было сказано значительно раньше. Это - в точности та схема, тот протокол взаимодействия, который мы рассматривали в статьях С чего начинается COM? и Именование потенциальных COM-объектов. И это - всё та же самая проблема, только уже при взгляде "изнутри", а не "снаружи". И способ её решения, фактически принятый в системе, тогда же и был показан. Только вот тогда ничего не говорилось ни об абстрактном классе, ни о том, что его надо как-то передавать серверу... Вылез там только какой-то riid... И лишь сейчас стало понятно почему он вылез - потому, что тогда мы как само собой разумеющееся считали, что "как объекты могут взаимодействовать друг с другом - они и сами знают". А на самом-то деле это было весьма голословное утверждение! Ведь способ их взаимодействия должен быть как-то описан компилятору на клиентской стороне, чтобы он мог правильно откомпилировать вызовы? И абстрактный класс для этого описания - очень подходящая кандидатура!

Всё бы хорошо, да вот... Абстрактный класс - понятие времени компиляции. А создание экземпляра статического типа - понятие времени выполнения, когда никакого абстрактного класса уже и в помине нет. Способ борьбы понятен - перенумеровать и абстрактные типы так же, как перенумеруются сами статические типы, и передавать серверу пару "номер статического типа - номер абстрактного типа". Только способ этот не сработает - именно потому, что абстрактный класс есть понятие времени компиляции. В откомпилированном модуле нет никаких классов, а есть "данные + код". Пока связывание было "ранним" - на этапе компиляции и линковки, такой способ годился. Потому, что все описания классов приводились компилятором к таблицам смещений, которые он сам же и подставлял в код. А сейчас у нас связывание "позднее" и те самые таблицы компилятора уже давно не существуют... Иными словами наш славный абстрактный класс не годится только потому, что он существует лишь "в воображении компилятора" и не существует во время исполнения. И "номер абстрактного типа" - номер сущности, которой нет в тот момент, когда на неё ссылаются. А есть ли "абстрактные структуры", существующие и во время исполнения тоже?

Есть! Есть такие структуры и самое парадоксальное, что они как раз и были придуманы в языке C++ для решения именно той самой проблемы, которую мы с таким трудом пытаемся решить - как сказать компилятору "как" не говоря "что". Как до поры разделить построение и использование объекта.

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

С++ - мощный язык. Мощный в том смысле, что он позволяет "по человечески естественно" выражать генетические отношения между статическими типами. Это - азбука C++ и на эту тему давно написано много, со вкусом, и в разных стилях. Так что повторять это здесь даже как-то и неудобно. И есть в C++ одна интересная особенность - в иерархии наследования можно определить такую конструкцию, что программа, располагающая объектом базового класса во время исполнения будет вызывать методы не самого этого, но производного от него класса. Традиционно этот случай иллюстрируется примером, когда определяется класс "геометрическая фигура", от него наследуются классы "круг", "треугольник", "квадрат" и в базовом классе определяется метод "рисовать фигуру". Но - какую именно фигуру класс "геометрическая фигура" не знает, зато это знают класс "круг" и другие - ведь они-то себя как раз рисовать и умеют. Как сделать по человечески совершенно естественную вещь - сделать так, чтобы один и тот же метод "рисовать фигуру", принадлежащий классу "геометрическая фигура", ничего не знающему о том, какие классы ещё только будут от него произведены, начал "рисовать фигуры" производных классов?

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

Поэтому общее решение возникшей перед нами проблемы как разделить сервер и клиент будет таким:

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

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


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

Конечно, COM - своего рода "побочное использование низкоуровневых конструкций реализации языка". Но принятое творцами языка решение небесспорно даже в системостроении на "натуральном C++". Когда абстрактные классы используются совсем не для обеспечения полиморфизма, а только лишь, как средство описать каркас классовой иерархии отдельно от её реализации эти указатели становятся излишними. А отключить их генерацию - нельзя. Microsoft несколько исправила положение введя в язык расширение __declspec(novtable), но оно - дополнительно и нестандартно.

предыдущий выпускархив и оглавлениеследующий выпуск
Авторские права © 2001 - 2004, Михаил Безверхов
Публикация требует разрешения автора

разделы сайта

форум

технология COM

оптимизация сайтов


© 2000-2004 Клуб программистов developing.ru