Применительно к долговременному хранению объектов существует специальный термин: эволюция схемы данных. Он означает, что с течением времени некоторые атрибуты хранимого объекта могут быть изъяты, переименнованы или добавлены. Некоторые атрибуты могут изменить свой тип (например, с unsigned char на unsigned short, с int на float и т.д.). Задачей базы данных является обеспечение наиболее безболезненой эволюции схемы данных. При этом база данных либо обеспечивает автоматическую (в крайнем случае автоматизированую) модификацию уже сохраненных данных, либо выполняет преобразование из старого формата в новый "на лету" в момент обращения к данным прикладных приложений.
Применительно к межпроцессовому взаимодействию проблема второй версии носит несколько иной характер. Как правило, существующий список атрибутов, их типы и размерности не изменяются. Поскольку это интерфейс, который был специфицирован и поддержан всеми взаимодействующими сторонами. Модификация интерфейса может потребовать изменение его реализации на каждой из взаимодействующих сторон. Что не всегда возможно. Если бы это было легко осуществимо, то проблемы второй версии бы не существовало -- просто все стороны одновременно перешли на использование нового интерфейса. Проблема второй версии в межпроцессовом взаимодействии как раз возникает, когда несколько из взаимодействующих сторон не могут перейти на использование новой версии интерфейса. Применение средств преобразования данных из одного формата в другой "на лету" так же может оказаться неприемлимым, например, из-за необходимых для этой операции вычислительных ресурсов.
Поэтому, при использовании сериализованных объектов для межпроцессового взаимодействия, наиболее распространненой практикой является расширение списка атрибутов, без изменения имен, типов и размерностей старых атрибутов. В этом случае возникает задача поддержки одновременного взаимодействия нескольких процессов, каждый из которых использует разные версии интерфейсов. Например, клиент 1 работает с версией #1, клиент 2 работает с версией #3, а сервер с версией #2.
Очевидны две проблемы:
A
, которое в версии #1 имеет атрибуты a, b, c. В версии #2 добавляются атрибуты d, e. А в версии #3 добавляются атрибуты f, g. Если клиент с версией #3 отправляет сообщение A
серверу с версией #2, то сервер должен получить сообщение с корректными значениями атрибутов a, b, c, d, e. Если же клиент с версией #1 отсылает сообщение клиенту с версией #3, то клиент с версией #3 должен получить сообщение с корректными значениями атрибутов a, b, c и с выставленными значениями по-умолчанию для атрибутов d, e, f, g.Как раз проблема гарантированного разбра сериализованного объекта безотносительно к тому, был ли он сериализован более старшей версией или нет, непосредственно касается oess_1::stdsn.
Ниже обсуждаются некоторые возможные способы решения данной проблемы, после чего описывается реализованный в oess v.1.2.0 механизм расширяемых (extensible) типов.
Ярким примером подхода на основе тегов являются способы сериализации ASN1 BER (Abstract Syntax Notation One Basic Encoding Rules), использующий механизм TLV (Tag, Length, Value) и сериализация с использованием какого-либо из языков разметки (XML, cls_2). В таком подходе каждому атрибуту предшествует тег, по которому определяется сериализованный атрибут. Поэтому не важно, в каком порядке атрибуты будут сериализованы. Так же не важно, все ли атрибуты будут сериализованы (т.к. легко определяются какие атрибуты присутствуют). Не критично и наличие неизвестных атрибутов, которые просто игнорируются.
Примером использования порядка следования атрибутов является ASN1 PER (Packed Encoding Rules). В нем сериализацией/десериализацией занимается вспомогательный код, сгенерированный на основе спецификации сериализуемого типа. Данный код упаковывает/распаковывает атрибуты объекта в том порядке, в котором они указаны в спецификации. В некоторых случаях в сериализованное представление помещаются специальные маркеры, указывающие присуствие/отсутствие значения конкретного атрибута. Такой подход становится зависимым от спецификации, но обеспечивает более высокую эффективность, как по скорости, так и по объемам необходимой памяти и генерируемого сериализуемого представления.
Сериализация в ObjESSty, с теоритической точки зрения, не завязана непосредственно ни на один из данных подходов. По DDL описанию строится вспомогательный код, который опрерирует атрибутами в том порядке, в котором они описаны в DDL. Но вспомогательный код не занимается непосресдственным формированием сериализованного образа объекта. Этим занимаются классы-сериализаторы сущностей. Данные классы реализуют интефейсы oess_1::stdsn::ient_t, oess_1::stdsn::oent_t и своей реализацией определяют, какой же механизм разметки должен использоваться.
Однако очевидно, что подобное использование вспомогательного кода в ObjESSty более близко к подходу ASN1 PER. Поэтому в oess v.1.2.0 существует всего две реализации сериализаторов сущностей, которые строят/разбирают безтеговое двоичное представление объекта: oess_1::stdsn::ient_std_t, oess_1::stdsn::oent_std_t.
Но в oess v.1.2.0 требуется, чтобы расширяемые типы были явно отмечены как расширяемые. Сделано это, главным образом, по двум причинам:
class handshake_t : public oess_1::stdsn::serializable_t { OESS_SERIALIZER( handshake_t ) private : // Сериализуемые атрибуты. unsigned int m_version; std::string m_client_id; ... };
// Класс добавлен в версии #2. class signature_setup_t : public oess_1::stdsn::serializable_t { OESS_SERIALIZER( signature_setup_t ) private : std::set< std::string > m_supported; std::string m_preferred; ... public : const std::string preferred_method; }; // Класс добавлен в версии #2. class compression_setup_t : public oess_1::stdsn::serializable_t { OESS_SERIALIZER( compression_setup_t ) private : std::set< std::string > m_supported; std::string m_preferred; unsigned char m_level; ... public : const std::string preferred_method; const unsigned char preferred_level; }; // Вторая версия класса. class handshake_t : public oess_1::stdsn::serializable_t { OESS_SERIALIZER( handshake_t ) private : // Сериализуемые атрибуты из первой версии ДОЛЖНЫ // остаться в неизменном виде. unsigned int m_version; std::string m_client_id; // Атрибуты из версии #2. signature_setup_t m_signature; compression_setup_t m_compression; ... };
// Класс добавлен в версии #3. class handshake_trait_t : public oess_1::stdsn::serializable_t { OESS_SERIALIZER( handshake_trait_t ) public : virtual int id() const = 0; ... }; // Класс добавлен в версии #3. class handshake_trait_shptr_t : public oess_1::stdsn::shptr_t { OESS_SERIALIZER( handshake_trait_shptr_t ) OESS_1_SHPTR_IFACE( handshake_trait_shptr_t, handshake_trait_t, oess_1::stdsn::shptr_t ) }; // Третья версия класса. class handshake_t : public oess_1::stdsn::serializable_t { OESS_SERIALIZER( handshake_t ) private : // Сериализуемые атрибуты из первой версии ДОЛЖНЫ // остаться в неизменном виде. unsigned int m_version; std::string m_client_id; // Атрибуты из версии #2 ДОЛЖНЫ остаться в неизменном виде. signature_setup_t m_signature; compression_setup_t m_compression; // Атрибуты из версии #3. std::map< int, handshake_trait_shptr_t > m_traits; ... };
|| Первая версия расширяемого типа. {type {extensible} handshake_t {attr m_version {of oess_1::uint_t}} {attr m_client_id {of std::string}} }
|| Класс добавлен в версии #2. {type {extensible} signature_setup_t {attr m_supported {stl-set} {of std::string} || Атрибут не обязательный. {default {c++ std::set< std::string >()} || Сериализуется только, если в нем есть значения. {present_if {c++ m_supported.size()}} } } {attr m_preferred {of std::string} || Атрибут не обязательный. {default {c++ preferred_method} || Сериализуется только, если заданы алгоритмы подписи || и значение отлично от значения по умолчанию. {present_if {c++ m_supported.size() && preferred_method != m_preferred}} } } } || Класс добавлен в версии #2. {type {extensible} compression_setup_t {attr m_supported {stl-set} {of std::string} || Атрибут не обязательный. {default {c++ std::set< std::string >()} || Сериализуется только, если в нем есть значения. {present_if {c++ m_supported.size()}} } } {attr m_preferred {of std::string} || Атрибут не обязательный. {default {c++ preferred_method} || Сериализуется только, если заданы алгоритмы подписи || и значение отлично от значения по умолчанию. {present_if {c++ m_supported.size() && preferred_method != m_preferred}} } } {attr m_level {of oess_1::uchar_t}} } || Вторая версия класса. {type {extensible} handshake_t {attr m_version {of oess_1::uint_t}} {attr m_client_id {of std::string}} || Описание расширения версии #2. {extension {attr m_signature {of signature_setup_t} {default {c++ signature_setup_t()}} } {attr m_compression {of compression_setup_t} {default {c++ compression_setup_t()}} } } }
|| Класс добавлен в версии #3. {type {abstract} handshake_trait_t} || Класс добавлен в версии #3. {type handshake_trait_shptr_t {super oess_1::stdsn::shptr_t} } || Третья версия класса. {type {extensible} handshake_t {attr m_version {of oess_1::uint_t}} {attr m_client_id {of std::string}} || Описание расширения версии #2. {extension {attr m_signature {of signature_setup_t} {default {c++ signature_setup_t()}} } {attr m_compression {of compression_setup_t} {default {c++ compression_setup_t()}} } || Описание расширения версии #3. {extension {attr m_traits {stl-map {key oess_1::int_t}} {of handshake_trait_shptr_t} {default {c++ std::map< int, handshake_trait_shptr_t >()} {present_if {c++ m_traits.size()}} } } } } }
Практика показывает, что может возникнуть необходимость расширить практически любой тип. Даже такой, который, казалось бы, не может быть расширен. Поэтому можно рекомендовать делать все типы расширяемыми, с учетом следующих исключений:
Так же в некоторых случаях можно оставлять не расширяемыми абстрактные типы. Например, в приведенном выше примере класс handshake_trait_t вряд-ли когда-нибудь будет иметь собственные атрибуты. Зато производные от него типы с большой вероятностью будут расширяемыми.