Классика баз данных - статьи

       

Модель компонентных объектов. Основные понятия.


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

Распределенная компонентная модель объектов (DCOM)- это (см.[1]) набор стандартов построения, размещения и взаимодействия компонент и реализующих их механизмов, которые позволяют объектам внутри компонент посылать вызовы другим компонентам, а также принимать и обрабатывать запросы от других компонент вне зависимости от их положения на компьютере или в сети, от способов реализации, от того, являются ли они прикладными или объектами операционной системы и т.д.
Для этого объекты (D)COM "договариваются" о предоставлении друг другу сервисов через строго определенные интерфейсы, которые на идеологическом уровне можно рассматривать как своего рода обязательство объекта предоставить заявленную функциональность при условии вызова в соответствии с опубликованными им правилами, а на бытовом- как группы семантически связанных функций, объединенных в абстрактные виртуальные классы. Пусть, например, имеем некоторый набор функций, оформленный в виде

struct I1 { virtual void f11()=0; virtual int& f12()=0; ... }

Любой класс, намеренный использовать эту функциональность, может быть оформлен, например, в виде наследования:

class AnyCls : public I1 ...

Однако, что предпринять, если нам потребовалось расширить набор функций? Переписать struct I1 означает вернуться к проблемам, описанным в п.1. Следовательно, логично будет свести дополнительные функции в новый интерфейс:

struct I2 { virtual void f21()=0; ... }

А как быть тогда с AnyCls? Интерфейсов может не один и не два, а множественное наследование выручает до известного предела, после которого возникают разного рода проблемы, начиная от слабой читаемости программы и заканчивая потерями в производительности. Решение состояло в использовании вложенных классов:

class AnyCls { protected: ... public: AnyCls {...;} class C1 : public I1 { public: C1() {} virtual void f11(); virtual int& f12(); ... } m1; class C2 : public I2 { public: C2() {} virtual void f21(); ... } m2; ... } void AnyCls::C1::f11(){ ... ;} ... , которые, с одной стороны, упакованы в родительском классе и не видны за его пределами, а с другой, имеют доступ к его элементам. Заметим, что с помощью функций CoCreateInstance или IUnknown::QueryInterface (см.ниже) клиент работает только с указателями на таблицы виртуальных функций интерфейсов, т.е. реально создав объект того же класса AnyCls, он тем не менее не имеет указателя на него. Таким образом он никогда не получает прямого доступа к внутренним данным объекта, даже если бы они не были protected.


Моделированию на С++ основных механизмов работы COM посвящена, например, [2], часть IV и желающие всегда могут обратиться к этой литературе, а также к [3], которая по справедливости считается основополагающей работой в области компонентного подхода, до сегодняшнего дня не утратившей своей актуальности.

Интерфейсы являются настолько основополагающим понятием COM, что для их описания используется специальный язык- IDL (язык определения интерфейсов), по своей структуре очень похожий на С++. В определении интерфейса описываются заголовки входящих в него функций с указанием их имен, возвращаемых типов, входных и выходных параметров, а также их типов. Это и есть та часть контракта, которая будет сообщать, что может и как надо вызывать объект, взявший на вооружение данный интерфейс. Каждый интерфейс получает свой 16-байтовый глобальный идентификатор, который присваивается ему программой генерации на основании времени создания, адреса сетевой платы и пр. После опубликования интерфейс фиксируется и дальнейшие изменения в нем не допускаются. IDL допускает наследование, так что возможны производные интерфейсы. Более того, все интерфейсы являются производными от одного базового интерфейса под названием IUnknown. В состав IUnknown входят 3 метода. QueryInterface используется для получения указателей на другие интерфейсы объекта, при условии, что указатель на начальный интерфейс был получен при помощи CoCreateInstance. Методы AddRef и Release применяются для подсчета ссылок, т.е., грубо говоря, сколько клиентов в данный момент используют интерфейсы данного объекта. AddRef выполняется автоматически, как только со стороны клиента поступает запрос на указатель интерфейса. Однако если внутри клиентской программы происходит порождение новой ссылки на интерфейс, о которой серверный объект не догадывается, вызов AddRef возлагается на клиента. Клиент не имеет права разрушить серверный объект, но по окончании работы с ним он обязан вызвать метод Release, который уменьшит на единичку число пользователей объекта.




Когда оно станет равным 0, объект сам себя уничтожит.

В том случае, если объект создан как in-process сервер (dll), т.е. выполняется внутри клиентского процесса, особых проблем не возникает. Если же объект реализован в виде out-of-process сервера, выполняющегося как отдельный процесс на том же или удаленном хосте, немедленно появляется вопрос о передаче указателя на интерфейсы и параметров вызовов методов между процессами. Вопрос нетривиальный тем более потому, что различные компьютеры могут использовать разные форматы представления данных. Ответ состоит в использовании proxy-объекта внутри клиентского процесса и заглушки внутри сервера. Proxy- это обычный COM-объект, который представляет те же интерфейсы, что и вызываемый клиентом, однако вместо непосредственного вызова методов он принимает переданные клиентом параметры и упаковывает их для передачи средствами межпроцессной или межмашинной (RPC- remote procedure call) коммуникации. Заглушка на стороне сервера принимает и распаковывает эти параметры, вызывает соответствующий метод серверного объекта и передает их ему. Возвращение результатов клиенту происходит по той же схеме в обратном направлении. Такой процесс называется маршалингом (демаршалингом). Обычно все развитые средства разработки COM-объектов предоставляют возможность автоматической генерации proxy и заглушки для интерфейса. Если разработчика по каким-то причинам это не устраивает, он может запрограммировать нестандартный маршалинг при помощи определенного в COM интерфейса IMarshal. Возникает интересный момент: если код маршалинга способен автоматически создаваться на стадии компиляции программы, то почему бы не использовать эту возможность для динамической генерации маршалеров во время выполнения программы? Такой подход (он известен как позднее связывание), несмотря на дополнительные затраты, существенно повышает гибкость программирования, ибо решение о вызовах тех или иных интерфейсов может приниматься по ходу дела в зависимости от прикладной логики.


Однако для этого клиент должен "на лету" уметь получать доступ к информации об интерфейсах, необходимой для выполнения маршалинга. Этим требованиям отвечает библиотека типов, которая перечисляет все интерфейсы, поддерживаемые тем объектом, для которого она создается, и описывается в терминах уже упоминавшегося нами выше IDL. Как и для COM-объектов, при установке в registry записывается ID библиотеки и ее местоположение. При вызове средствами СОМ API библиотека типов предоставляет интерфейс ITypeLib, который, в свою очередь, позволяет получить указатели на интерфейс ITypeInfo для каждого интерфейса, перечисленного в библиотеке, с помощью которых добывается информация о параметрах методов и их типов, необходимая для динамического маршалинга.

Обычно основным потребителем этой информации оказывается интерфейс IDispatch (естественно, производный от IUnknown), который наверняка знаком программистам на Visual Basic. Методы этого интерфейса GetTypeInfo и GetTypeInfoCount позволяют динамически запрашивать запущенный объект относительно информации о всех его интерфейсах. Другой метод, IDispatch::Invoke фактически представляет собой оператор switch, который в зависимости от переданного значения идентификатора (DISPID) вызывает тот или иной метод диспинтерфейса. Иногда в литературе можно встретить суждение, что Visual Basic не работает с указателями, поэтому для него потребовалось создать некий универсальный механизм, умеющий работать только с одним интерфейсом IDispatch. Это не совсем корректное замечание. Во-первых, в свете нашего разговора о маршалинге очевидно, что единственная пара proxy-заглушка для IDispatch не в состоянии предусмотреть преобразование параметров для самых различных методов диспинтерфейса, поэтому часть преобразования неявно выполняет сам клиент, упаковывая параметры в variant. Во-вторых, только первая версия Visual Basic распознавала исключительно диспинтерфейсы. Во всех остальных случях QueryInterface совершенно аналогично возвращает указатель на виртуальную таблицу, содержащую реализацию запрашиваемого интерфейса.


В терминах Visual Basic это известно как объектная ссылка (object reference). Т.е., если переменная объявлена как dim x As <Something>, где <Something>№ Object, то будет использоваться раннее связывание. В первую очередь компилятор (VB5) будет искать способы прямого вызова через виртуальную таблицу (vtable binding) и только в случае неудачи прибегнет к диспинтерфейсу. При этом по информации в библиотеке типов он постарается найти DISPID и кэшировать его, чтобы сэкономить на достаточно дорогом вызове GetIDsOfNames в период выполнения. При ссылках типа dim x As Object или Set x = y, где создание объекта y происходит во время работы программы, компилятор, естественно, лишен возможности сделать какие-либо предположения о природе объектов х и не сможет оптимизировать их вызовы. В этом случае, как нетрудно догадаться, будет иметь место позднее связывание. Позднее связывание преимущественно характерно для Visual FoxPro 5.0, в него также включена оптимизация для поддержки vtable binding- см. функцию sys(2333). Иногда интерфейсы описываются как двойственные, т.е. содержащие диспинтерфейсные представления для виртуальных таблиц своих методов.

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


Во-вторых, пользователь мыслит в тех же категориях, что и автор, что значительно облегчает освоение программы. В третьих- и это плюс, характерный для всей технологии COM,- предоставленная функциональность остается на вооружении пользователя при разработке им каких-то своих программ, никак не связанных с данным OLE Automation сервером. Простой пример: если у нас на компьютере установлен Microsoft Office, то зачем нам изобретать велосипед и писать программу проверки орфографии, если мы можем создать объект Word.Application и вызвать для него метод SpellChecking:

dim x As New Word.Application Debug.Print x.SpellChecking("Abra Cadabra") ...

То же самое относится, например, к использованию статистических функций Microsoft Excel и т.д. В качестве OLE Automation серверов могут рассматриваться не только офисные приложения, но и сами средства разработки- тот же Visual Basic или Visual FoxPro- и даже тяжелые серверные продукты семейства Microsoft BackOffice, например, Microsoft SQL Server, который с помощью SQL-DMO (distributed management objects) обеспечивает выполнение практически всех административных функций из клиентского приложения (разумеется, при наличии соответствующих прав доступа).

dim oSQLServer As New SQLOLE.SQLServer oSQLServer.Connect "ntalexejs", "sa"

dim newdb As New SQLOLE.Database newdb.Name = "sqlole"

newdb.ExtendOnDevices ("oledat=5") newdb.TransactionLog.DedicateLogDevices ("olelog=2")

oSQLServer.Databases.Add newdb


Содержание раздела