Наши проекты:
Журнал · Discuz!ML · Wiki · DRKB · Помощь проекту |
||
ПРАВИЛА | FAQ | Помощь | Поиск | Участники | Календарь | Избранное | RSS |
[3.236.86.184] |
|
Сообщ.
#1
,
|
|
|
Ну что ж, решил поделиться с вами своими наработками в области сериализации в Delphi. Данная тема всегда вызывала у меня интерес и со временем мой взгляд на то, как это должно быть, эволюционировал и в итоге пришел к тому, что будет продемонстировано ниже. Хочу заметить, что методика, используемая мной, чем-то напоминает .NET-овскую сериализацию, правда менее наворочена, однако была разработана независимо и любые совпадения - случайность Так же замечу, что приведенная реализация в версиях Delphi 2010 и старше может быть с легкостью модернизирована до такой степени, что код сериализуемых объектов в принципе будет переносим на платформу .NET - для этого предлагается научить сериализатор работать с атрибутами классов и их членов (SerializableAttribute, NonSerializedAttribute и т.д.).
Определения Что же такое сериализация? Сериализация - процесс перевода какой-либо структуры данных (например объекта) в последовательность байтов. Обратный процесс восстановления структуры данных из последовательности байт называется десериализация. Сериализация используется в тех случаях, когда нужно сохранить объект в файл, память, базу данных, или передать по сети, с возможностью в дальнейшем восстановить объект в исходном состоянии. Особенности Итак, прежде чем приступить к рассмотрению, краткий обзор особенностей данной реализации: Итак, перейдем к рассмотрению реализации. Дерево состояния объекта Сериализация в предоставляемой библиотеке реализована в два этапа, с использованием промежуточного дерева состояния между объектом и потоком данных. Т.е. при записи параметры объекта сначала записываются в промежуточное дерево и лишь потом - в файл или поток, а при чтении - сначала из файла или потока формируется дерево состояния, а лишь потом по нему восстанавливается объект. Такое разделение процессов сериализации/десериализации позволяет: Дерево состояния объекта представлено интерфейсом ITreeNode и классом TTreeNode, этот интерфейс реализующий. Вот объявление ITreeNode: // Базовый интерфейс узла дерева 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. Вот его объявление: // Базовый класс сериализатора объектов 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; Методы класса можно разделить на группы. По типу хранилища: По типу аргумента: Только два метода этого класса абстрактные - это методы, формирующие данные в потоке (TStream) по дереву и наоборот - дерево по данным из потока, т.е. методы отвечающие за формат хранения данных. Все остальные методы реализованы в классе TSerializer Потомки класса перекрывают эти методы, обеспечивая хранение данных в соответствии с заданным форматом. Реализованы два потомка - для бинарного формата и для XML. Сериализация объекта Для примера сериализации рассмотрим класс, содержащий настройки программы. // Класс, содержащий настройки приложения 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; Класс объявляет три опубликованных свойства, что автоматически означает, что эти свойства будут записываться и читаться при сериализации/десериализации. Код записи этого класса в файл с помощью сериализатора будет выглядеть следующим образом: 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; Записанный файл будет выглядеть так: <?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 будет таким: <?xml version="1.0" encoding="windows-1251"?> <TApplicationSettings> <Splash> <Show>True</Show> <Timeout>2000</Timeout> </Splash> <UserProfile>user.dat</UserProfile> </TApplicationSettings> Директива stored так же позволяет управлять сериализуемостью свойства. Настраиваемая сериализация Как уже было сказано выше, не всегда возможно или целесообразно предоставлять сериализуемые данные в виде published-свойств. В этом случае класс может реализовать интерфейс ICustomSerialize. Вот его объявление: // Интерфейс настраиваемой сериализации ICustomSerialize = interface(IInterface) ['{BB7DC15E-3D67-4183-B758-F3CDF493A513}'] procedure CustomSerialize(const Node: ITreeNode; Revision: Integer); procedure CustomDeSerialize(const Node: ITreeNode); end; Для примера уберем свойства ShowSplash и SplashTimeout из секции published, и сериализуем их, используя названные механизмы. // Класс, содержащий настройки приложения 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. // Интерфейс уведомления о сериалицации ISerializeNotify = interface(IInterface) ['{9ED646CF-1C5E-48B2-A5C5-3191A8758BDB}'] procedure BeginSave; procedure EndSave; procedure BeginLoad; procedure EndLoad; end; Реализация этого интерфейса приведет к тому, что его методы будут вызываться при сериализации/десериализации (до и после), что позволит объекту обработать эти события, если это необходимо. Контроль версий объектов Последнее что осталось рассмотреть - это контроль версий. Бывают случаи, когда программа используется долго и структура объектов меняется. Тем не менее, хочется чтобы новые программы успешно читали файлы старых форматов. Для решения этой задачи предлагается следующий механизм. Сразу же хочу заметить, что если изменение в структуре объекта - это просто появление нового свойства, то вероятнее всего прибегать к этому механизму не придется. Если в дереве состояния не найдется записи о новом свойстве, то ошибки времени выполнения не возникнет, а значение свойства останется прежним, каким было до десериализации. Но если же структура объекта меняется значительно, то избежать использования данных механизмов проблематично. Итак, чтобы реализовать в сериализуемом объекте поддержку контроля версий, необходимо выполнить следующие шаги: Интерфейс IRevision: // Интерфейс управления ревизиями 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-методах сериализатора - это приведет к тому, что объект будет записан в соответствии с указанной ревизией. Второй этап, который необходимо реализовать - это создать контроллер ревизии. Класс контроллера объявлен так: // Базовый класс контроллеров ревизии 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; Рассмотрим методы этого класса: Ну и когда класс контроллера ревизии написан, его необходимо зарегистрировать в менеджере ревизий: var Mgr: TRevisionsManager; begin Mgr := TRevisionsManager.GetInstance; Mgs.RegisterController(MyRevisionControllerClass); Ну вот собственно и все. Файлы исходных кодов классов прилагаются, замечания/предложения приветствуются Прикреплённый файлSerialization.rar (8,58 Кбайт, скачиваний: 19) |