Редакция благодарна господину Охотникову за внимательное чтение нашего журнала и за активное отношение к опубликованным статьям. С согласия автора письма мы решили опубликовать его (с некоторыми сокращениями). Однако, как и опубликованная нами статья, письмо, по нашему мнению, содержит некоторые спорные доводы. Поэтому мы сопровождаем публикацию письма комментариями (с которыми, вероятно, тоже согласятся не все).
Сергей КузнецовВ №3 за 1999 год журнала «Открытые системы» была опубликована довольно неоднозначная статья В.Шринивасана, Д.Т.Чанга «Долговременное хранение объектов в объектно-ориентированных приложениях». Данная статья вызвала у меня, как у разработчика собственной объектной СУБД, настолько сильное впечатление, что я впервые решился высказать свои мысли как по поводу конкретно этой статьи, так и по поводу освещения ситуации с объектными СУБД в отечественной компьютерной прессе.
Охотников Евгений АнатольевичО конкретной статье
Статья «Долговременное хранение объектов в объектно-ориентированных приложениях» является обзором состояния дел в области хранения объектно-ориентированных данных. Данная статья идеально подходит для читателей, не знающих о проблемах организации хранения объектно-ориентированных данных.
С точки зрения разработчика объектной СУБД статья содержит много неоднозначностей, что свидетельствует либо о поверхностном отношении авторов к данной проблеме, либо о не совсем верном переводе.
Прочитав эту часть письма, я понял, что комментировать его можно только имея под руками и перевод, и оригинальный текст. Кстати, исходный текст статьи можно найти по адресу http://www.research.ibm.com/journal/sj/361/srinivasan.html (IBM Systems Journal, 36, 1, 1998). Я не стал бы упрекать авторов статьи в поверхостном отношении к проблеме, но, возможно, они попытались охватить слишком широкий диапазон подходов. Ведь эта статья не об организации объектных СУБД, а о способах поддержки долговременно хранимых объектов в приложениях. Тема кажется нам настолько актуальной и настолько мало раскрытой, что мы считаем публикацию статьи на русском языке полезной и правильной. Перевод, на самом деле, точно соответствует оригиналу. Другое дело, что, видимо, его стоило сопроводить комментариями. Однако в любом случае в объектном мире всегда отсутствовало согласие. В частности, нет общепринятой терминологии, и даже фундаментальные понятия объектно-ориентированного подхода (идентифицируемость, наследование, полиморфизм, позднее связывание и т.д.) до сих пор понимаются по-разному в разных лагерях исследователей и разработчиков.
В пункте «Идентичность объектов» говорится о необходимости поддержки идентификаторов объектов (OID) на уровне СУБД. Но, во-первых, само название «Идентичность объектов» не выражает сути проблемы. Точнее было бы использовать название «Идентификация объектов». Однако, в статье два разных понятия «идентичность» и «идентификация» рассматриваются как одно целое (сначала говорится об OID, а затем о том, что записи с одинаковыми значениями могут представлять разные объекты).
Термин Object Identity, отражающий, по мнению объектного мира, одно из фундаментальнейших понятий OOP, традиционно представлял трудность при поиске русского эквивалента. Основная сложность состоит в дуализме смысла этого термина. С одной стороны, конечно, речь идет о том, что каждый созданный объект получает создаваемый соответствующей системой уникальный идентификатор (OID), не изменяемый в течение всей жизни объекта. Как подчеркивают авторы статьи, для долговременно хранимых объектов их OID?ы тоже являются долговременными. С другой стороны, мне кажется правильным специальное подчеркивание того факта, что два объекта с совершенно одинаковыми значениями переменных состояния, но с разными OID, являются разными объектами. Хотя этот пример со служащими и их одинаковыми автомобилями, возможно, не является самым удачным.
Во-вторых, не показывается, почему идентификация объектов настолько важна. Идентификация объектов необходима для организации навигационного (по ссылкам) доступа к данным. Например, пусть некоторый объект-контроллер управляется при помощи некоторого объекта-порта ввода-вывода. Пусть для инициализации контроллера необходимо записать в управляющий порт определенное значение. На C++ это можно выразить так:
class controll_io_port_t { public : ... void write( int value ) { ... } }; class controller_t { protected : controll_io_port_t * m_controllPort; ... public : ... void init( void ) { m_controllPort-> write( 0x501 ); ... } };
В данном примере используется ссылка-указатель на объект «управляющий порт-ввода вывода». Пока речь идет о хранении объектов ОП данное решение представляется вполне обычным. Но, если появляется необходимость хранения объектов во внешней памяти, то возникает вопрос о способе представления ссылки. Если СУБД поддерживает идентификацию объектов, то ссылка может представлятся идентификатором объекта в БД:
class controller_t { protected : /* Ссылка в ОП */ controll_io_port_t * m_controllPort; /* Ссылка в БД */ persistent OID m_oid_controllPort; ... public : ... void init( void ) { m_controllPort->write( 0x501 ); ... } };
В этом случае для доступа к объекту «управляющий порт-ввода вывода» достаточно преобразовать OID в указатель. Большинство объектных СУБД выполняют это преобразование автоматически и добавляют специализированные атрибуты (в данном примере m_oid_controllPort) в пользовательские классы совершенно прозрачно для пользователя.
Однако возможны и другие способы организации ссылок. Пусть каждый контроллер и каждый порт ввода-вывода имеют собственные имена. Причем имя порта ввода-вывода строится из имени контроллера и суффикса «_io_port». Тогда пример можно представить на некотором несуществующем диалекте C++ следующим образом:
class controll_io_port_t { protected : char m_name[ 50 ]; public : ... void write( int value ) { ... } }; class controller_t { protected : char m_name[ 20 ]; ... public : ... void init( void ) { /* Создаем имя контроллера и получаем ссылку на порт ввода-вывода */ char port_name[ 50 ]; sprintf( port_name, «%s_io_port», m_name ); /* Это не C++ */ (controll_io_port*) port_name)->write( 0x501 ); ... } };
В этом примере осуществляется доступ к объекту по значению. Именно такой подход лежит в основе реляционных и объектно-реляционных СУБД. Данный пример показывает, что при организации хранения объектно-ориентированных данных можно обойтись без идентификации объектов в БД. Вопрос упирается в производительность. Использование идентификаторов объектов обеспечивает большую производительность, поскольку заранее известно какой объект необходимо извлечь из БД.
Эта часть письма кажется мне весьма характерной для многих авторов-практиков. Почему-то они считают, что примеры на С++ сразу делают все абсолютно ясным. Позвольте изложить мою точку зрения. Идентификаторы объектов имеют много аналогов как в языках программирования, так и в традиционных базах данных. Если не вдаваться в схоластические споры, то ближайшим аналогом объекта в языках программирования является типизированная переменная. Если предположить, что в языке допускается определение типов пользователями и поддерживается строгая типизация, то со значениями этой переменной (которые могут быть произвольно сложными) можно работать только посредством операций ее типа (та самая инкапсуляция, о которой мы еще поговорим). Переменная может обладать, а может и не обладать (если она динамическая) именем, но всегда обладает адресом (указательным значением в смысле C/C++). Именно этот адрес является ближайшим аналогом OID. Значения переменной могут изменяться, но адрес ее сохраняется, пока она существует. Две переменные с различными адресами различны, даже если их значения совпадают. В традиционных реляционных базах данных понятным аналогом OID является первичный ключ кортежа отношения. Также понятно и отличие: OID объекта определяется системой и, по крайней мере на концептуальном уровне, не хранится при объекте. Первичный ключ отношения определяется пользователем (проектировщиком базы данных), его значения сохраняются в кортежах, а система лишь обеспечивает проверку уникальности значений первичного ключа для каждого отношения. Хочу напомнить, что уже очень давно были предложены расширения реляционной модели (например, RMT Криса Дейта), в которых уникальные идентификаторы кортежей (суррогаты) генерируются системой. И обратите внимание, что абсолютно непринципиально то, хранятся ли значения суррогатов в кортежах или нет. Принципиальна уникальная идентифицируемость. Конечно же, я должен согласиться с автором письма в том, что наличие уникальных идентификаторов позволяет организовывать произвольно сложные структуры данных со ссылками и осуществлять в них явную навигацию, но, по моему мнению, более важен тот фундаментальный факт, что все моделируемые в приложении или базе данных объекты внешнего мира (здесь я использую термин «объект» в житейском смысле) различны и каким-то образом уникально идентифицируемы, даже если они несут одну и ту же информацию. Теперь немного поговорим про имена. Здесь я полностью согласен с подходом CORBA: OID?ы объектов первичны, a служба именования вторична и может быть организована как надстройка. Грубо говоря, для того, чтобы организовать службу именования в объектной системе, достаточно иметь один специальный объект с заранее известным OID, методы которого позволяют сопоставлять именам других объектов их OID?ы. Эффективность доступа к объектам по именам будет зависить от эффективности этого специализированного объекта. Кстати, заметим, что эффективность доступа к объектам по OID тоже не очевидна и зависит от особенностей реализации конкретной системы.
При обсуждении составных объектов неожиданно и без объяснений используется понятие набора (специфичное для сетевых СУБД): «Как правило, составные объекты используются в приложениях как группы объектов, которые являются частью родительского объекта, который, в свою очередь, является обычно набором». Лично для меня, данная фраза значительно усложнила определение составного объекта.
В оригинале эта фраза звучит так: Typically object-oriented applications utilize a composite object as a group of objects that are part of a parent object that is typically a collection. Слово «коллекция» всегда было неприятным для переводчиков. Возможно, использование перевода «набор» не удачно, хотя, в принципе, коллекция - это набор. Может быть, наоборот, более неудачным было использование слова «набор» как русского эквивалента для «set» в литературе про Codasyl. В любом случае, наш перевод достаточно точно соответствует оригиналу, который, в свою очередь, вводит не идеальное, но разумное определение.
При обсуждении удаления составных объектов использованы несколько предложений, которые говорят либо о неточном переводе, либо о непонимании авторами термина «extent»: «В Versant расширение класса можно рассматривать как составной объект, в котором собраны все экземпляры класса. Удаление объекта автоматически приведет к удалению их из расширения класса. В результате определения расширения для класса удаляются все экземпляры класса». Если extent в объектных СУБД - это множество всех экземпляров, то удаление объекта должно приводить к его удалению из extent, а удаление extent должно приводить к удалению всех экземпляров класса. Но причем же здесь составные объекты?
Во-первых, в цитируемом абзаце статьи имеется досадная опечатка (именно опечатка, а не ошибка переводчика). Последнее предложение следует читать следующим образом: «В результате удаления расширения класса будут удалены все экземпляры класса». Во-вторых, я не понимаю вопроса автора письма. Да, extent класса можно трактовать как составной объект со своими методами, содержащий множество существующих экземпляров этого класса. Почему бы и нет? И в этом случае естественно, что уничтожение объекта приводит к его удалению из соответствующего расширения класса, а уничтожение объекта-extent?a должно приводить к уничтожению всех объектов, соодержащихся в нем. Фактически, иллюстрируется каскадное уничтожение подобъектов сложного объекта.
Под инкапсуляцией в объектно-ориентированном подходе принято считать объединение данных и обрабатывающего их кода Если рассматривать пункт «Инкапсуляция» с этих позиций, то получится, что авторы имеют в виду что-то другое. Точка зрения авторов становится понятной, если под инкапсуляцией понимать доступ к данным класса только через доступные методы класса. В сильно упрощенном варианте это означает, что для каждого атрибута класса должно существовать два доступных метода: метод для получения значения и метод для изменения значения атрибута. Нечто подобное используется в технологии SOM.
С моей точки зрения, понятие инкапсуляции в ООП в точности унаследовано от языков с абстрактными типами данных (ADT). На модельном уровне это в точности то, что говорят авторы статьи про четкое разделение интерфейса доступа к переменным и значениям ADT и внутренней структуры, используемой для реализации ADT. Действительно, в переложении этого понятия в терминах ООП к объекту любого класса можно обращаться только через интерфейс этого класса (т.е. через набор публичных методов этого класса). Заметим, что возможность наличия публично доступных атрибутов объектов не противоречит приведенному утверждению, поскольку в этом случае упоминаемые автором письма методы get и put могут определяться для таких атрибутов автоматически. В реализациях долговременно хранимых объектов возникают разные трудности в связи с инкапсуляцией. В частности, внутренняя структура объектов должна быть открыта, например, для оптимизатора запросов к ООБД. Кроме того, многие разработчики ООСУБД полагают, что хотя строгая инкапсуляция должна поддерживаться на уровне приложений, она противоречит специфике непредвиденных (ad hoc) запросов к базе данных.
Если рассматривать инкапсуляцию с позиций объектно-ориентированного подхода, то возникает вопрос о месте хранения кода методов класса и их привязки к объектам БД. Частично, ответ на этот вопрос дается в пункте «Переопределение, совмещение и динамическое связывание».
Мне кажется, что этот раздел статьи является предельно четким, хотя, следуя традициям, наверное, следовало переводить «overloading» как «перегрузку», а не как «совмещение».
При обсуждении наследования неоднократно употребляется понятие переносимости относительно различных платформ, но не говорится о переносимости чего и относительно чего идет речь. Переносимость БД относительно приложений, реализованных на разных языках программирования? Перенос файлов БД на компьютер с другой аппаратной архитектурой? Переносимость приложений относительно различных СУБД? Если речь идет о приложениях, созданых на разных языках программирования, то накладываются одни органичения, в том числе и на наследование. Если речь идет о переносимости приложений, написанных на языке C++ под другую операционную систему, то ограничений на наследование вообще нет.
В этом месте авторы статьи действительно не совсем точны (кстати переносимость и расширяемость упоминаются всего один раз). Поскольку упоминается технология SOM, то, по всей видимости, речь идет о создании распределенных объектных приложений, компоненты которых могут выполняться на разных платформах. И в этой связи авторы подчеркивают большую гибкость (и меньшую эффективность) модели наследования на основе операций по сравнению со структурной моделью. Идея-то понятна, поскольку наследование на основе операций более свойственно интерпретируемым средам выполнения (например, Smalltalk), а в этом случае портирование программ упрощается. Речь не идет про смену СУБД или про использование конкретных языков программирования. Говорится про модели наследования, свойственные разным объектным моделям.
Слабо обсуждается множественное наследование. Виртуальное множественное наследование вообще не упоминается, хотя для некоторых случаев оно чрезвычайно важно. Но в статье даже не сказано, поддерживают ли виртуальное множественное наследование обсуждаемые средства.
Снова должен сказать, что авторы обсуждают модели наследования, а не реализации. Понятно, что если бы они занялись деталями механизмов наследования, то написали бы совсем другую (может быть, более интересную для специалистов) статью, а возможно, и книгу.
В статье термин «полиморфизм» заменяется терминами «динамического связывания» и «позднего связывания». Если учитывать возможность применения динамически-загружаемых библиотек (DLL), то такая замена терминов является недопустимой. Термин полиморфизм означает, что выяснение метода, подлежащего вызову, откладывается на момент вызова метода. При этом полиморфизм очень сильно связан с понятием наследования. Рассмотрим пример на C++:
class A{ public : virtual void f( void ) { cout << "A::f()" << endl; } }; class B : public A { public : void f( void ) { cout << "B::f()" << endl; } }; /* Функция, в которой производится обращение к методу f() */ void call_f( A & a ) { /* Здесь имеет место полиморфизм */ a.f(); } void main( void ) { A a; B b; call_f( a ); call_f( b ); }
В данном примере полиморфизм состоит в том, что в функции call_f определение метода, подлежащего вызову (A::f или B::f), произойдет только на этапе выполнение программы.
Динамическое или позднее связывание может существовать и использоваться одновременно с полиморфизмом. Для примера можно представить, что метод A::f обращается к функции action_a(), а метод B::f обращается к функции action_b(). Функции action_a и action_b находятся в динамически-загружаемой библиотеке actions.dll. В этом случае для платформы Win32 функции action_a и action_b могут выполнять один вид действий (например, рассылать сообщения по сети), а для платформы OS/2 - другой вид действий (например, опрашивать периферийное обурудование). В данном примере полиморфизм отвечает за выбор метода (A::f или B::f), а динамическое связывание - за загрузку необходимой динамически загружаемой библиотеки.
Начну с того, что хотя в названии раздела действительно используется термин «динамическое связывание», в тексте употребляется более привычный для ООП термин «позднее связывание» (late binding). Термин «полиморфизм» действительно не используется в статье, возможно, чтобы не пугать неискушенных читателей сильно научными словами. На самом-то деле, речь действительно идет о двух разновидностях полиморфизма связанных, соответственно с возможностями переопределения и перегрузки методов. Переопределение метода возможно при определении подкласса на основе одного (простое наследование) или нескольких (множественное наследование) суперклассов. Переопределенный метод должен иметь ту же сигнатуру (имя + список имен типов параметров), что и переопределяемый метод. Поскольку в соответствии с принятой семантикой наследования с любым объектом подкласса можно работать как с объектом любого его суперкласса, код используемого метода может быть неизвестен во время компиляции его вызова. Отсюда возникает потребность в позднем связывании, т.е. определении реализации метода во время выполнения программы. Другими словами, позднее связывание - это способ разрешения полиморфизма методов, возникающего за счет возможности их переопределения при наследовании. Перегрузка метода - это воозможность определения в том же классе одноименного метода с другим набором типов формальных параметров. Таким образом все одноименные перегруженные методы обладают разными сигнатурами. Это позволяет разрешать полиморфизм методов, возникающий за счет перегрузки, на стадии компиляции. С моей точки зрения, возможность динамической загрузки, упоминаемая автором письма, здесь вообще не при чем.
При обсуждении навигации фактически обсуждаются способы загрузки объектов БД в оперативную память, но не говорится о том, что индивидуальная загрузка объектов БД в ОП является основной чертой объектных СУБД. В частности, быстрый доступ к объектам при использовании ObjectStore приписывается тому, что представления объекта в БД и в ОП идентичны. В действительности, при работе с объектными СУБД объекты загружаются в ОП и находятся там до момента выгрузки. Именно за счет этого достигается высокая скорость работы. Причем каждая из СУБД использует собственные механизмы загрузки и выгрузки объектов. Например, в ObjectStore этот механизм настолько прозрачен для пользователя, что пользователь даже не подозревает о его существовании. В других СУБД, например в POET, пользователю необходимо заботится о явной загрузке-выгрузке объектов.
Раздел, посвященный навигации, действительно написан авторами статьи не совсем четко. На самом деле, стоило бы затронуть такие аспекты, как кэширование объектов в основной памяти сервера, кэширование на стороне клиента и их использование в основной памяти приложения.
При обсуждении эволюции схемы данных авторами делается слишком спорное и слишком категоричное утверждение о том, что автоматизировать процесс эволюции схемы данных в объектных СУБД полностью невозможно. При этом авторы не раскрывают причин появления такого утверждения.
Действительно, можно выделить, как минимум, две причины, по которым можно сделать такое утверждение.
Во-первых, существует два различных подхода к описанию изменения схемы данных. Наиболее простой подход состоит в использовании элементарных, однозначно трактуемых команд: «изменить тип атрибута такого-то на такой-то», «добавить в такой-то класс такой-то атрибут такого-то типа», «изъять такой-то класс из списка базовых типов такого-то класса» и т.д. Такой подход применяется в интерактивных системах изменения схемы данных (особенно в реляционных СУБД). Но насколько данный способ прост в реализации, настолько же он сложен и неудобен применительно к объектно-ориентированным данным.
Более сложный поход состоит в сравнении двух описаний схем данных (старой и новой) и выявлении изменений (в конечном счете должен появится набор команд первого типа). Причем его эффективность зависит от сложности и интеллектуальности алгоритма выявления изменений. Данный подход наиболее применим для объектно-ориентированных данных, структура которых описывается в виде деклараций классов на каком-либо языке.
Алгоритмы выявления изменений достаточно сложны, но вполне реализуемы. Более того, они уже используются в существующих объектных СУБД.
Во-вторых, существуют случаи, которые не были предусмотрены при реализации механизма эволюции схемы данных. В этих случаях действительно требуется «ручное» преобразование представления объектов. Например, если требуется преобразовать целочисленный атрибут «инвентарный номер» в строковый атрибут «инвентарный номер», причем необходимо сохранить старое значение целочисленного атрибута, добавив префикс «№». Однако такое преобразование схемы данных потребует ручного вмешательства для любого способа организации долговременного хранения.
Исходя из вышеизложенного, можно сказать, что авторы слишком строго и не совсем верно оценили возможности объектных СУБД по поддержке эволюции схемы данных.
Если говорить более точно, авторы статьи слишком коротко сформулировали свое утверждение, не пояснив, что в точности имеют в виду. Но снова заметим, что статья относится к более широкой теме, чем организация ООСУБД.
О постсоветских объектных СУБД
Как бы это не было невероятно, но в бывшем Советском Союзе давно и активно развиваются объектные СУБД. Лично мне известны четыре разработки:
GoodBase (Боровицкий М.Д., Смирнов С.В., Реализация и исследование производительности объектно-ориентированной СУБД // Программирование, 1992, №6, сс.18-22.);
«Шаман» (Смирнов А., Беляков А., Бронников Г., Филимонов Ю., Чаянов Н. Зачем нужны объектно-ориентированные СУБД // Компьютер Пресс, 1995, №9, сс.46-51.);
ODB-Jupiter (http://www.inteltec.ru);
Dss (http://gsu.unibel.by/people/ ohotnikov).
Данные разработки совершенно различны, выполнялись в разное время и применялись для различных задач (GoodBase - для решения задач в металургии, ODB-Jupiter - для создания систем хранения и поиска документов, Dss - для создания систем контроля и управления технологическими процессами). Это говорит о том, что у нас также накоплен большой опыт применения объектных СУБД. Но, к сожалению, публикации про отечественные объектные СУБД появляются крайне редко и носят, в основном, рекламно-информационный характер. Если предоставить разработчикам отечественных объектных СУБД возможность поделится своим опытом, то может получится более грамотный, предметный и полезный обмен мнениями по данному поводу.
Редакция «Открытых систем» всячески приветствует оригинальные статьи. К сожалению, не все разработчики любят и/или умеют их писать. Наш критерий - интересный и качественный текст.