oess_1.2.0. Расширяемые типы

Проблема версий

Стандартный механизм сериализации oess_1::stdsn имеет два основных применения: долговременное хранение информации в oess_1::db и для реализации обмена данными между несколькими процессами. В обоих случаях возникает проблема, известная как "проблема второй версии". Заключается она в том, что с течением времени спецификация сериализуемого типа нуждается в изменении, требуется выпустить "вторую" версию спецификации.

Применительно к долговременному хранению объектов существует специальный термин: эволюция схемы данных. Он означает, что с течением времени некоторые атрибуты хранимого объекта могут быть изъяты, переименнованы или добавлены. Некоторые атрибуты могут изменить свой тип (например, с unsigned char на unsigned short, с int на float и т.д.). Задачей базы данных является обеспечение наиболее безболезненой эволюции схемы данных. При этом база данных либо обеспечивает автоматическую (в крайнем случае автоматизированую) модификацию уже сохраненных данных, либо выполняет преобразование из старого формата в новый "на лету" в момент обращения к данным прикладных приложений.

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

Поэтому, при использовании сериализованных объектов для межпроцессового взаимодействия, наиболее распространненой практикой является расширение списка атрибутов, без изменения имен, типов и размерностей старых атрибутов. В этом случае возникает задача поддержки одновременного взаимодействия нескольких процессов, каждый из которых использует разные версии интерфейсов. Например, клиент 1 работает с версией #1, клиент 2 работает с версией #3, а сервер с версией #2.

Очевидны две проблемы:

Как раз проблема гарантированного разбра сериализованного объекта безотносительно к тому, был ли он сериализован более старшей версией или нет, непосредственно касается 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.

Необходимость явной декларации расширяемости

Как было сказано выше, в ObjESSty существует возможность использовать один и тот же вспомогательный код для формирования принципиально разных представлений объектов. Теговые представления не нуждаются в явной декларации расширяемости типа. Безтеговые представления, строго говоря так же не нуждаются в этом, т.к. вполне можно реализовать такие схемы сериализации, которые в любом случае помещают в сериализованное представление признак наличия/существования расширения объекта.

Но в oess v.1.2.0 требуется, чтобы расширяемые типы были явно отмечены как расширяемые. Сделано это, главным образом, по двум причинам:

Описание расширяемых типов на C++

На уровне языка C++ нет никакой разницы между расширяемыми и не расширяемыми типами. Расширение типа заключается в том, что создается новая версия C++ описания типа, в которое включаются новые атрибуты. Например:

Версия #1
class handshake_t : public oess_1::stdsn::serializable_t
{
  OESS_SERIALIZER( handshake_t )
  private :
    // Сериализуемые атрибуты.
    unsigned int  m_version;
    std::string m_client_id;
  ...
};
Версия #2
Добавляется описание механизма компрессии и подписи данных.
// Класс добавлен в версии #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
Добавляется механизм передачи дополнительных свойств (traits), список которых предполагается дополнять в будущем.
// Класс добавлен в версии #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;
  ...
};

Описание расширяемых типов на DDL

В описании на DDL существуют следующие правила:

Заметки:
В версии 1.2.0 не накладывается ограничений на то, где должен располагаться тег {extension} - до списка атрибутов, среди описаний атрибутов или после атрибутов. Но рекомендуется указывать тег {extension} последним тегом своего родительского тега (т.е. указывать его всегда после списка атрибутов). Т.к. возможно, что в будущих версиях ObjESSty расположение тега {extension} будет иметь важное значение.
Приведенный в предыдущей секции пример записывается на DDL следующим образом:

Версия #1
|| Первая версия расширяемого типа.
{type {extensible}	handshake_t
	{attr	m_version {of oess_1::uint_t}}
	{attr	m_client_id {of std::string}}
}
Версия #2
Добавляется описание механизма компрессии и подписи данных.
|| Класс добавлен в версии #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
Добавляется механизм передачи дополнительных свойств (traits), список которых предполагается дополнять в будущем.
|| Класс добавлен в версии #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()}}
				}
			}
		}
	}
}

Значения по-умолчанию для атрибутах в расширениях

При описании в DDL атрибутов в расширениях очень важно указывать значение по-умолчанию для каждого такого атрибута. Дело в том, что при десериализации объекта ObjESSty назначает значения только тем атрибутам, которые были обнаружены в сериализованном представлении. Остальные атрибуты остаются без изменения. Это может стать проблемой, например, в таком случае: объекту A было присвоено какое-то значение. Затем в объект A была осуществлена десериализация некоторого двоичного представления. Если двоичное представление было создано предыдущей версией, то в объекте A часть атрибутов получат новые значения, но атрибуты из последнего расширения изменены не будут. Т.е. объект A не будет корректным образом объекта из двоичного представления, поскольку отсутствовавшие в этом представлении атрибуты не получили значений по-умолчанию.

Какие типы нужно делать расширяемыми

ObjESSty требует, чтобы возможность расширения была заложена в самой первой версии описания типа. Если этого не сделать, то двоичные представления объектов разных версий одного типа, полученных с помощью oess_1::stdsn::oent_no_markers_t будут несовместимы друг с другом. Поэтому важно, чтобы при создании самой первой версии спецификации типа было известно, допускает ли тип расширения или нет.

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

Так же в некоторых случаях можно оставлять не расширяемыми абстрактные типы. Например, в приведенном выше примере класс handshake_trait_t вряд-ли когда-нибудь будет иметь собственные атрибуты. Зато производные от него типы с большой вероятностью будут расширяемыми.


Документация по ObjESSty. Последние изменения: Fri Oct 13 18:35:37 2006. Создано системой  doxygen 1.4.7
Hosted by uCoz