На главную
ПРАВИЛА FAQ Помощь Участники Календарь Избранное DigiMania RSS
msm.ru
Модераторы: jack128, Rouse_, Krid
  
    > Сериализация объектов, почти как в .NET
      Ну что ж, решил поделиться с вами своими наработками в области сериализации в Delphi. Данная тема всегда вызывала у меня интерес и со временем мой взгляд на то, как это должно быть, эволюционировал и в итоге пришел к тому, что будет продемонстировано ниже. Хочу заметить, что методика, используемая мной, чем-то напоминает .NET-овскую сериализацию, правда менее наворочена, однако была разработана независимо и любые совпадения - случайность :) Так же замечу, что приведенная реализация в версиях Delphi 2010 и старше может быть с легкостью модернизирована до такой степени, что код сериализуемых объектов в принципе будет переносим на платформу .NET - для этого предлагается научить сериализатор работать с атрибутами классов и их членов (SerializableAttribute, NonSerializedAttribute и т.д.).

      Определения
      Что же такое сериализация? Сериализация - процесс перевода какой-либо структуры данных (например объекта) в последовательность байтов. Обратный процесс восстановления структуры данных из последовательности байт называется десериализация. Сериализация используется в тех случаях, когда нужно сохранить объект в файл, память, базу данных, или передать по сети, с возможностью в дальнейшем восстановить объект в исходном состоянии.

      Особенности
      Итак, прежде чем приступить к рассмотрению, краткий обзор особенностей данной реализации:
      • Любой объект может быть сериализован. Для этого класс должен предоставить информацию о сериализуемых параметрах, а саму сериализацию на основе предоставленной информации выполняет специальный объект - сериализатор.
      • Предоставление информации для сериализатора возможно двумя путями. Первый - это объявить published-свойства. Такое объявление приведет к записи значения этого свойства при сериализации. Свойство может иметь объектных тип, в этом случае для записи этого свойства-объекта так же будет применен сериализатор, что позволяет использовать сериализатор для записи не только одиночных объектов, но и графов объектов. Второй способ применяется в случае, когда записываемые параметры нельзя или нецелесообразно предоставлять в виде published-свойств. В этом случае объект может реализовать специальный интерфейс настраиваемой сериализации, который позволяет записывать и читать произвольные дополнительные данные.
      • Отсутствует привязка к конкретному формату хранения данных. Объект только лишь объявляет, какие параметры необходимо сериализовать, но не уточняет в каком виде. Вопрос о конечном формате данных зависит от типа используемого сериализатора. Если был использован сериализатор XML, то данные будут записаны в XML-виде, если бинарный сериализатор - то в бинарном виде. Один и тот же объект можно записать в файлы разных форматов просто применив к нему разные типы сериализаторов
      • Существует механизм уведомлений объектов о процессе сериализации/десериализации. Данный механизм может быть использован для осуществления каких-либо действий над объектом до или после сериализации/десериализации
      • Реализован механизм управления ревизиями (версиями). Если структура данных объекта претерпела изменения в процессе развития программы, система позволит объектам читать файлы старых форматов, и наоборот - записывать данные в старых форматах в целях совместимости.

      Итак, перейдем к рассмотрению реализации.
      Дерево состояния объекта
      Сериализация в предоставляемой библиотеке реализована в два этапа, с использованием промежуточного дерева состояния между объектом и потоком данных. Т.е. при записи параметры объекта сначала записываются в промежуточное дерево и лишь потом - в файл или поток, а при чтении - сначала из файла или потока формируется дерево состояния, а лишь потом по нему восстанавливается объект. Такое разделение процессов сериализации/десериализации позволяет:
      • Отвязаться от конкретного формата данных используя как промежуточную стадию одинаковое для всех форматов дерево
      • Реализовать в базовом классе сериализаторов операции по формированию дерева из состояния объекта и наоборот - восстановлению объекта из дерева, а операции записи дерева в поток и чтения дерева из потока переложить на потомков
      • Использовать запись в дерево и чтение из дерева вообще не используя никакие конкретные форматы данных - удобно если не нужно длительно хранить состояние объекта, а скажем скопировать или клонировать объект путем записи одним объектом своего состояния в дерево, а вторым - чтения из этого дерева. В данных операциях трата времени и ресурсов на формирование данных опеределенного типа (XML и т.д.) совершенно излишне.

      Дерево состояния объекта представлено интерфейсом ITreeNode и классом TTreeNode, этот интерфейс реализующий. Вот объявление ITreeNode:
      ExpandedWrap disabled
          // Базовый интерфейс узла дерева
          ITreeNode = interface(IUnknown)
          ['{9CD610C0-9F03-405B-B655-86B49E582DD3}']
            // Имя узла
            function GetName: WideString; safecall;
            procedure SetName(const Value: WideString); safecall;
            property Name: WideString read GetName write SetName;
            // Родитель
            function GetParent: ITreeNode; safecall;
            property Parent: ITreeNode read GetParent;
            // Список дочерних подузлов
            function GetNodesCount: Integer; safecall;
            function GetNode(Index: Integer): ITreeNode; safecall;
            function GetNodeByName(AName: WideString): ITreeNode; safecall;
            function GetNodeIndex(AName: WideString): Integer; safecall;
            property NodesCount: Integer read GetNodesCount;
            property Nodes[Index: Integer]: ITreeNode read GetNode;
            property NodeByName[AName: WideString]: ITreeNode read GetNodeByName;
            property NodeIndex[AName: WideString]: Integer read GetNodeIndex;
            // Список параметров узла
            function GetParamsCount: Integer; safecall;
            function GetParamName(Index: Integer): WideString; safecall;
            function GetParamValue(Index: Integer): OleVariant; safecall;
            procedure SetParamValue(Index: Integer; const Value: OleVariant); safecall;
            function GetParamValueByName(AName: WideString): OleVariant; safecall;
            procedure SetParamValueByName(AName: WideString; const Value: OleVariant); safecall;
            function GetParamIndex(AName: WideString): Integer; safecall;
            property ParamsCount: Integer read GetParamsCount;
            property ParamName[Index: Integer]: WideString read GetParamName;
            property ParamValue[Index: Integer]: OleVariant read GetParamValue write
              SetParamValue;
            property ParamValueByName[AName: WideString]: OleVariant read GetParamValueByName
              write SetParamValueByName;
            property ParamIndex[AName: WideString]: Integer read GetParamIndex;
            // Управление узлами
            function AddNode(AName: WideString): ITreeNode; safecall;
            function InsertNode(Index: Integer; AName: WideString): ITreeNode; safecall;
            procedure DeleteNode(Index: Integer); overload; safecall;
            procedure DeleteNode(AName: WideString); overload; safecall;
            procedure DeleteNode(Node: ITreeNode); overload; safecall;
            procedure ClearNodes; safecall;
            function NodeExists(AName: WideString): Boolean; safecall;
            procedure Assign(const Node: ITreeNode); safecall;
            procedure Merge(const Node: ITreeNode); safecall;
            // Управление параметрами
            procedure AddParam(AName: WideString; const AValue: OleVariant); safecall;
            procedure InsertParam(Index: Integer; AName: WideString; const
              AValue: OleVariant); safecall;
            procedure DeleteParam(Index: Integer); overload; safecall;
            procedure DeleteParam(AName: WideString); overload; safecall;
            procedure ClearParams; safecall;
            function ParamExists(AName: WideString): Boolean; safecall;
            // Полная очистка
            procedure Clear; safecall;
          end;

      Объявление выглядит громоздким, но ничего сложного в этом типе нет. Любой узел дерева может содержать дочерние узлы (Nodes) и параметры (Params). Узлы и параметры имеют строковое имя, параметры имеют кроме имени вариантное значение. Методы интерфейса - это методы управления узлами и параметрами. Типы WideString и OleVariant использованы для совместимости интерфейса с COM/OLE.
      При сериализации, данные объекта содержатся в узлах и параметрах дерева.

      Класс сериализатора
      Как уже было сказано выше, чтобы записать или прочитать состояние объекта из файла или потока, к нему нужно применить методы сериализатора. Сериализатор - это класс, который содержит методы записи/чтения состояния объекта. Базовым для всех типов сериализаторов является класс TSerializer. Вот его объявление:
      ExpandedWrap disabled
          // Базовый класс сериализатора объектов
          TSerializer = class(TObject)
          public
            class procedure SaveToNode(AObject: TObject; const Node: ITreeNode;
              PreferStr: Boolean = True; Revision: Integer = -1);
            class procedure LoadFromNode(AObject: TObject; const Node: ITreeNode);
            class procedure SaveToStream(const Node: ITreeNode; Stream:
              TStream); overload; virtual; abstract;
            class procedure LoadFromStream(const Node: ITreeNode;
              Stream: TStream); overload; virtual; abstract;
            class procedure SaveToStream(AObject: TObject; Stream: TStream;
              PreferStr: Boolean = True; Revision: Integer = -1); overload;
            class procedure LoadFromStream(AObject: TObject; Stream: TStream); overload;
            class procedure SaveToFile(const Node: ITreeNode; FileName: String); overload;
            class procedure LoadFromFile(const Node: ITreeNode;
              FileName: String); overload;
            class procedure SaveToFile(AObject: TObject; FileName: String;
              PreferStr: Boolean = True; Revision: Integer = -1); overload;
            class procedure LoadFromFile(AObject: TObject; FileName: String); overload;
          end;

      Методы класса можно разделить на группы.
      • Методы записи. Начинаются с SaveTo
      • Методы чтения. Начинаются с LoadFrom
      По типу хранилища:
      • Работающие с файлом. Заканчиваются на File. Вызывают методы работы со Stream, передавая параметром TFileStream
      • Работающие с потоком. Заканчиваются на Stream.
      • Работающие с деревом состояния. Заканчиваются на Node.
      По типу аргумента:
      • Работающие с объектом. Первый параметр типа TObject. При реализации формируют дерево и вызывают методы, работающие с ITreeNode
      • Работающие с деревом состояния. Первый параметр типа ITreeNode
      Только два метода этого класса абстрактные - это методы, формирующие данные в потоке (TStream) по дереву и наоборот - дерево по данным из потока, т.е. методы отвечающие за формат хранения данных. Все остальные методы реализованы в классе TSerializer
      Потомки класса перекрывают эти методы, обеспечивая хранение данных в соответствии с заданным форматом. Реализованы два потомка - для бинарного формата и для XML.

      Сериализация объекта
      Для примера сериализации рассмотрим класс, содержащий настройки программы.

      ExpandedWrap disabled
        // Класс, содержащий настройки приложения
        TApplicationSettings = class(TPersistent)
        ...
        published
          property ShowSplash: Boolean read FShowSplash write FShowSplash;
          property SplashTimeout: Integer read FSplashTimeout write FSplashTimeout;
          property UserProfile: String read FUserProfile write FUserProfile;
        end;


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

      ExpandedWrap disabled
        var
          Settings: TApplicationSettings;
        begin
          Settings := TApplicationSettings.Create;
          try
            Settings.ShowSplash := True;
            Settings.SplashTimeout := 2000;
            Settings.UserProfile := 'user.dat';
            // Записываем состояние объекта в файл 'Settings.xml'
            TXmlSerializer.SaveToFile(Settings, 'Settings.xml');
          finally
            Settings.Free;
          end;
        end;


      Записанный файл будет выглядеть так:

      ExpandedWrap disabled
        <?xml version="1.0" encoding="windows-1251"?>
        <TApplicationSettings>
          <ShowSplash>True</ShowSplash>
          <SplashTimeout>2000</SplashTimeout>
          <UserProfile>user.dat</UserProfile>
        </TApplicationSettings>


      Сериализатор позволяет сохранять не только published-свойства простых типов, но и свойства объектных типов, в этом случае объектное свойство будет сериализовано во вложенный узел. Например, если свойства ShowSplash и SplashTimeout перенести в объектное свойство Splash: TSplashSettings, у которого Show и Timeout будут published-cвойствами, то результирующий XML будет таким:

      ExpandedWrap disabled
        <?xml version="1.0" encoding="windows-1251"?>
        <TApplicationSettings>
          <Splash>
            <Show>True</Show>
            <Timeout>2000</Timeout>
          </Splash>
          <UserProfile>user.dat</UserProfile>
        </TApplicationSettings>


      Директива stored так же позволяет управлять сериализуемостью свойства.

      Настраиваемая сериализация
      Как уже было сказано выше, не всегда возможно или целесообразно предоставлять сериализуемые данные в виде published-свойств. В этом случае класс может реализовать интерфейс ICustomSerialize. Вот его объявление:
      ExpandedWrap disabled
          // Интерфейс настраиваемой сериализации
          ICustomSerialize = interface(IInterface)
          ['{BB7DC15E-3D67-4183-B758-F3CDF493A513}']
            procedure CustomSerialize(const Node: ITreeNode; Revision: Integer);
            procedure CustomDeSerialize(const Node: ITreeNode);
          end;


      Для примера уберем свойства ShowSplash и SplashTimeout из секции published, и сериализуем их, используя названные механизмы.

      ExpandedWrap disabled
        // Класс, содержащий настройки приложения
        TApplicationSettings = class(TInterfacedPersistent,
          ICustomSerialize)
        ...
        protected
          // ICustomSerialize
          procedure CustomSerialize(const Node: ITreeNode; Revision: Integer);
          procedure CustomDeserialize(const Node: ITreeNode);
        public
          property ShowSplash: Boolean read FShowSplash write FShowSplash;
          property SplashTimeout: Integer read FSplashTimeout write FSplashTimeout;
        published
          property UserProfile: String read FUserProfile write FUserProfile;
        end;
         
        implementation
          
        procedure TApplicationSettings.CustomSerialize(const Node: ITreeNode;
          Revision: Integer);
        var
          SplashNode: ITreeNode;
        begin
          SplashNode := Node.AddNode('Splash');
          SplashNode.AddParam('Show', ShowSplash);
          SplashNode.AddParam('Timeout', SplashTimeout);
        end;
         
        procedure TApplicationSettings.CustomDeserialize(const Node: ITreeNode);
        var
          SplashNode: ITreeNode;
        begin
          SplashNode := Node.NodesByName['Splash'];
          ShowSplash := SplashNode.ParamValueByName['Show'];
          SplashTimeout := SplashNode.ParamValueByName['Timeout'];
        end;


      Результат будет такой же, как и в предыдущем примере.

      Сериализация массивов, списков и коллекций
      Интерфейс настраиваемой сериализации наиболее часто нужен для реализации коллекций или свойств-массивов. Можно даже специально реализовать класс, инкапсулирующий список или массив, реализующий ICustomSerialize, и потом просто объявлять его как published для записи коллекции как составной части объекта. Реализация такого класса не представляет особой сложности - можно рассматривать его в качестве домашнего задания :)

      Уведомления о сериализации
      Любой сериализуемый объект может реализовать интерфейс ISerializeNotify.
      ExpandedWrap disabled
          // Интерфейс уведомления о сериалицации
          ISerializeNotify = interface(IInterface)
          ['{9ED646CF-1C5E-48B2-A5C5-3191A8758BDB}']
            procedure BeginSave;
            procedure EndSave;
            procedure BeginLoad;
            procedure EndLoad;
          end;

      Реализация этого интерфейса приведет к тому, что его методы будут вызываться при сериализации/десериализации (до и после), что позволит объекту обработать эти события, если это необходимо.

      Контроль версий объектов
      Последнее что осталось рассмотреть - это контроль версий. Бывают случаи, когда программа используется долго и структура объектов меняется. Тем не менее, хочется чтобы новые программы успешно читали файлы старых форматов. Для решения этой задачи предлагается следующий механизм. Сразу же хочу заметить, что если изменение в структуре объекта - это просто появление нового свойства, то вероятнее всего прибегать к этому механизму не придется. Если в дереве состояния не найдется записи о новом свойстве, то ошибки времени выполнения не возникнет, а значение свойства останется прежним, каким было до десериализации. Но если же структура объекта меняется значительно, то избежать использования данных механизмов проблематично.
      Итак, чтобы реализовать в сериализуемом объекте поддержку контроля версий, необходимо выполнить следующие шаги:
      • Реализовать в объекте интерфейс IRevision.
      • Создать контроллер ревизии (потомок класса TRevisionController) для каждой версии объекта
      • Зарегистрировать контроллер в менеджере ревизий (TRevisionsManager)
      Интерфейс IRevision:
      ExpandedWrap disabled
          // Интерфейс управления ревизиями
          IRevision = interface(IInterface)
          ['{8370E426-0A0E-4B69-B180-7A8E5067A59F}']
            function GetRevision: Integer;
            property Revision: Integer read GetRevision;
            procedure WriteRevision(const Node: ITreeNode; Revision: Integer);
            function ReadRevision(const Node: ITreeNode): Integer;
          end;

      Довольно простой интерфейс, содержит всего три метода: GetRevision должен вернуть номер текущей ревизии объекта. Метод WriteRevision - записывает ревизию в дерево состояния (например можно записать в параметр "Revision"). Метод ReadRevision - читает номер ревизии объекта из дерева. В последнем методе можно предусмотреть случай, когда номер ревизии не записан в дерево, т.е. файл был сохранен когда разных ревизий еще не было, и возвращать нулевой номер ревизии.
      Номер ревизии можно указать в Save-методах сериализатора - это приведет к тому, что объект будет записан в соответствии с указанной ревизией.
      Второй этап, который необходимо реализовать - это создать контроллер ревизии. Класс контроллера объявлен так:
      ExpandedWrap disabled
          // Базовый класс контроллеров ревизии
          TRevisionController = class(TObject)
          protected
            class procedure Upgrade(const Node: ITreeNode; AObject:
              TObject); virtual; abstract;
            class procedure Downgrade(const Node: ITreeNode; AObject:
              TObject); virtual; abstract;
            class function GetNestedObjectRevision(AObject: TObject; Revision: Integer):
              Integer; virtual;
          public
            class function Revision: Integer; virtual; abstract;
            class function PrevRevision: Integer; virtual;
            class function NextRevision: Integer; virtual;
            class function ObjectClass: TClass; virtual; abstract;
            class function Critical: Boolean; virtual;
            class function RevisionName: String; virtual;
          end;

      Рассмотрим методы этого класса:
      • Revision - возвращает номер ревизии, для которой реализован контроллер. Должен быть перекрыт
      • PrevRevision - номер предыдущей ревизии. В базовой реализации возвращает Revision - 1
      • NextRevision - номер следующей ревизии. В базовой реализации возвращает Revision + 1
      • ObjectClass - возвращает класс объекта, для которого реализован контроль ревизий (класс объекта, который изменил свою структуру). Должен быть перекрыт
      • Critical - описательный метод, с его помощью можно указать что изменения критические, т.е. не поддерживаются предыдущие версии. В базовой реализации возвращает False
      • RevisionName - описательный метод, позволяет указать текстовое описание ревизии (можно использовать в окнах диалога сохранения)
      • Upgrade - один из двух важнейших методов. В своей реализации должен преобразовать дерево, записанное в формате предыдущей ревизии, в дерево, записанное в соответствии с форматом текущей ревизии. Т.е. добавить в дерево новые параметры или изменить структуру записи старых в соответствии с новым форматом. Используется менеджером ревизий при чтении файлов старых форматов. Должен быть перекрыт.
      • Downgrade - один из двух важнейших методов. В своей реализации должен преобразовать дерево, записанное в соответствии с форматом текущей ревизии, в дерево, записанное в формате предыдущей ревизии. Используется менеджером ревизий при записи файлов старых форматов. Должен быть перекрыт.
      • GetNestedObjectRevision - опциональный метод, в случае если записываемый объект содержит вложенные объекты, позволяет указать номер ревизии для них.

      Ну и когда класс контроллера ревизии написан, его необходимо зарегистрировать в менеджере ревизий:
      ExpandedWrap disabled
        var
          Mgr: TRevisionsManager;
        begin
          Mgr := TRevisionsManager.GetInstance;
          Mgs.RegisterController(MyRevisionControllerClass);


      Ну вот собственно и все. Файлы исходных кодов классов прилагаются, замечания/предложения приветствуются
      Прикреплённый файлПрикреплённый файлSerialization.rar (8,58 Кбайт, скачиваний: 19)
      Фашисты будущего будут называть себя антифашистами. У. Черчилль
      1 пользователей читают эту тему (1 гостей и 0 скрытых пользователей)
      0 пользователей:


      Рейтинг@Mail.ru
      [ Script Execution time: 0,1298 ]   [ 16 queries used ]   [ Generated: 23.10.17, 20:41 GMT ]