Rambler's Top100
"Knowledge itself is power"
F.Bacon
Поиск | Карта сайта | Помощь | О проекте | ТТХ  
 Подземелье Магов
  
 

Фильтр по датам

 
 К н и г и
 
Книжная полка
 
 
Библиотека
 
  
  
 


Поиск
 
Поиск по КС
Поиск в статьях
Яndex© + Google©
Поиск книг

 
  
Тематический каталог
Все манускрипты

 
  
Карта VCL
ОШИБКИ
Сообщения системы

 
Форумы
 
Круглый стол
Новые вопросы

 
  
Базарная площадь
Городская площадь

 
   
С Л С

 
Летопись
 
Королевские Хроники
Рыцарский Зал
Глас народа!

 
  
ТТХ
Конкурсы
Королевская клюква

 
Разделы
 
Hello, World!
Лицей

Квинтана

 
  
Сокровищница
Подземелье Магов
Подводные камни
Свитки

 
  
Школа ОБЕРОНА

 
  
Арсенальная башня
Фолианты
Полигон

 
  
Книга Песка
Дальние земли

 
  
АРХИВЫ

 
 

Сейчас на сайте присутствуют:
 
  
 
Во Флориде и в Королевстве сейчас  03:17[Войти] | [Зарегистрироваться]

Delphi и персистентность — новый взгляд

Юрий Спектор
дата публикации 22-09-2008 09:37

Delphi и персистентность — новый взгляд

Всякое абстрагирование есть не что иное, как
устранение некоторых ясных представлений,
которое обычно только для того и предпринимают,
чтобы тем яснее представить себе остающееся.
И. Кант

В данной статье речь пойдет о персистентности — возможности, которая существует в Delphi с момента его появления на свет, заложена непосредственно в язык и компилятор, и на которой построена вся идея визуального проектирования. А реализована эта возможность в простом классе стандартной runtime-библиотеки — TPersistent. Однако в данной статье эта стандартная реализация будет подвержена некоторой критике и в качестве альтернативы — попытаюсь предложить свою.

Спрашивается, а зачем это все нужно? Класс TPersistent — один из фундаментальных классов Delphi RTL, на нем построена вся VCL. Огромное количество существующего кода используют персистентность именно в таком виде, зачем нужен очередной "велосипед", чьи принципы к тому же идут в разрез со стандартными, и соответственно — не может быть применены к уже существующим потомкам TPersistent без дополнительной доработки? Ну, если бы я считал, что никакой пользы в этом нет, то не стал бы зря тратить свое и ваше время. Как мне кажется, приведенная в статье реализация придает дополнительную гибкость, открывает новые возможности, которыми вы можете успешно воспользоваться в собственных проектах. А если же готовое решение, приведенное в данной статье, вас удовлетворять не будет, то, по крайней мере, попытаюсь поделиться своими мыслями, идеями, которые возможно в ваших умах получат дальнейшее развитие. Ну и вдохновением, разумеется.

Статья разбита на две части. Первая — "Критика чистого разума", содержит подробное объяснение идеи данной статьи, а вторая — "Критика практического разума", демонстрирует возможности предлагаемого решения на конкретных примерах.

КРИТИКА ЧИСТОГО РАЗУМА

Что я должен знать?

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

Стандартные механизмы сериализации изначально предназначались для поддержки визуального проектирования. В случаях, когда мы имеем дело не с компонентами на форме или модуле данных, а со своими классами, стандартный функционал может быть избыточен и неудобен. Более того, стандартные механизмы сериализации жестко привязаны к формату записываемых данных. Если нам нужно записать состояние объекта в формате XML, скажем, или каком-либо другом, отличном от "родного" для стандартной библиотеки формата, мы столкнемся с определенными трудностями. И в дополнение ко всему мне кажется, возможности, открываемые персистентностью, могут быть шире тех, что предоставляет класс TPersistent. К счастью, несмотря на то, что стандартная runtime-библиотека нам такой возможности не предоставляет, язык Delphi обладает необходимым инструментарием для реализации персистентности в том виде, в котором нам этого захочется.

Итак, теперь давайте на время забудем о существовании TPersistent и попытаемся спроектировать свой класс, в который будет заложена возможность определять и работать с состоянием объекта.

Что я должен делать?

Простейший класс с поддержкой персистентности можно было бы реализовать так:

TMyPersistent = class(TObject)
public
  procedure SaveToStream(Stream: TStream); virtual; abstract;
  procedure LoadFromStream(Stream: TStream); virtual; abstract;
end;

Все очень просто — базовый класс объявляет абстрактные методы записи/чтения состояния, которые потомки должны перекрыть, где и реализовать конкретное поведение, необходимое для данного класса. В чем достоинство такого подхода? Полиморфное поведение — нам не нужно знать конкретный тип экземпляра, достаточно знать, что он унаследован от TMyPersistent. В чем же недостаток? В том, что для каждого конкретного класса нам придется самим писать реализацию загрузки и сохранения. Сколько в проекте классов, для которых мы хотим задать такое поведение — столько и реализаций. Это во-первых. Во-вторых, реализация одного и того же класса может меняться с развитием проекта — соответственно придется менять и код загрузки/сохранения. И в-третьих, классы меняются от проекта к проекту, следовательно говорить о повторном использовании кода не уместно. Итог — увеличенное время и стоимость разработки и дальнейшей поддержки кода.

Какие есть альтернативы? Альтернатива кроется в смысле слова "рефлексия". Заглянем в учебник по психологии и прочтем, что рефлексия — это "мысль направленная на мысль, обращенность сознания на самого себя". Расшифруем это "познай самого себя" в более близких для программиста понятиях. Если персистентность — это способность хранить объектом свое состояние, то рефлексия — это способность объекта обращаться к самому себе, посмотреть на себя со стороны, для того, чтобы узнать, что именно определяет его состояние. Представьте, как если бы любой объект мог взглянуть на объявление своего класса в юните и сразу же определить, что именно является его частями и как они должны быть записаны в поток. Тогда бы нам было достаточно просто написать код получения объектом его составных частей (один раз для всех классов) и далее в цикле просто сохранить каждую из частей в поток.

Приведем псевдокод построенного на данной концепции класса с поддержкой персистентности:

TMyPersistent = class(TObject)
public
  procedure SaveToStream(Stream: TStream);
  procedure LoadFromStream(Stream: TStream); 
end;
...
TMyPersistent.SaveToStream(Stream: TStream);
begin
  for <для каждой части объекта> do
    <Сохранить>(<часть>, Stream);
end;

TMyPersistent.LoadFromStream(Stream: TStream);
begin
  for <для каждой части объекта> do
    <Загрузить>(<часть>, Stream);
end;

В данном псевдокоде мы просто проходим в цикле по всем составным частям объекта и сохраняем/загружаем их из потока.

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

Итак, объекту нужны некоторые таблицы с данными о самом себе, обратившись к которым, он может узнать все необходимое. В терминах некоторых языков такие таблицы называются таблицами метаданных — т.е. данные о данных. В Delphi же устоялся термин RTTI (runtime type information) — информация о типах, доступная во время выполнения, что по сути — то же самое.

Откуда берется эта информация? Ее генерирует компилятор и помещает в скомпилированном коде в секцию доступную только для чтения. Из любого участка программы мы можем обратиться к этим таблицам и получить нужную нам информацию.

Что эта информация включает? В случае классов, наиболее важное для нас заключается в том, что в этих таблицах есть достаточно полное описание свойств (property) класса, объявленных как published — их имена, типы и некоторые другие параметры.

Как попросить компилятор сгенерировать для данного класса такие таблицы? Для этого класс должен быть скомпилирован с директивой {$M+} либо бы наследником такого класса. В стандартной библиотеке Delphi это требование как раз выполняется для класса TPersistent и его наследников.

Примечание: Членами класса являются не только данные, но и методы для их обработки. С помощью недокументированной директивы {$METHODINFO ON} можно заставить компилятор сгенерировать подробную информацию о public и published методах класса, что позволит во время выполнения генерировать вызовы этих методов, подобно тому, как это происходит в скриптовых интерпретаторах. Узнать об этом подробнее можно, изучив модули ObjAuto.pas и ObjComAuto.pas.

Более подробную информацию по RTTI в Delphi и как с ней работать приводить не будем, для тех кто не знаком — на эту тему есть множество статей и книг. В данном случае для нас важен сам факт существования такой информации и возможности работать с ней.

Вернемся к последней реализации класса TMyPersistent и обсудим ее достоинства и недостатки. Главное достоинство в том, что мы оторвались от конкретики и поднялись на следующий уровень абстракции. Теперь нам не нужно определять изменчивое поведение конкретных классов, а достаточно реализовать общую неизменную абстрактную концепцию, придерживаясь которой, каждому классу достаточно лишь объявить, что именно характеризует его внутреннее состояние (используя зарезервированные слова языка published, default, stored), а писать код записи и чтения — не нужно.

Теперь поговорим о недостатках. Первый недостаток заключается в том, что не всегда можно описать состояние объекта только лишь его опубликованными свойствами. Может быть более сложный случай, когда нужно уметь сохранять дополнительные сущности — например, класс TBitmap, содержащий двоичные данные растрового изображения. Второй недостаток в том, что у нас класс TMyPersistent жестко привязан к формату хранения данных, так как код записи/чтения из потока реализован в классе непосредственно. Если мы захотим хранить состояние объекта в разных форматах (скажем, в одном проекте нам нужна бинарная сериализация, в другом — XML), нас ждут большие трудности.

Бороться с этими недостатками можно. С первым — это предоставить классу возможность записывать дополнительные произвольные данные, помимо опубликованных свойств. Можно это реализовать в виде виртуальных методов, которые дополнительно вызывать в SaveToStream/LoadFromStream.

Как бороться со вторым недостатком? Можно переложить ответственность по взаимодействию с потоком на внешний для объекта класс. Таких классов можно сделать несколько — по одному на каждый необходимый формат, а от персистентного объекта потребовать только способность правильно предоставить информацию о своем состоянии в каком-либо общем виде.

Значит, объект не должен писать в поток непосредственно. Между объектом и потоком должна быть еще одна сущность — абстрактное дерево состояния, в каждом узле которого будет храниться свойство и его значение. Почему дерево? Потому что объекты могут быть вложены друг в друга. Почему абстрактное? Потому что объект не должен знать, кто реализует это дерево и как оно в дальнейшем будет использовано, объект будет знать лишь его интерфейс.

TMyPersistent = class(TObject)
public
  procedure SaveToTree(Tree: ITree);
  procedure LoadFromTree(Tree: ITree); 
end;

Мы можем написать множество классов, предоставляющий интерфейс дерева, которые абстрагируют нас от конкретного хранилища. Так один класс может скрывать за собой документ XML, второй — INI-файл, третий — реестр Windows, четвертый — таблицы базы данных и т.д. Объект просто должен уметь заполнить это дерево, занеся в него свое состояние, а также уметь восстановить свое состояние из дерева. А так у нас есть рефлексия, реализовать такой метод мы можем непосредственно в классе TMyPersistent раз и навсегда.

В приведенном чуть выше коде параметр Tree методов записи/чтения имеет тип ITree, первая буква "I" в названии которого говорит об интерфейсном типе, а не о классовом. В принципе, интерфейс или класс — не имеет значения, просто объявив параметр как интерфейс, мы не ставим жесткие рамки, требующие чтобы класс, реализующий дерево, был потомком некого TTree. Это может быть совершенно любой класс, главное чтобы он предоставлял реализацию интерфейса ITree.

На что я смею надеяться?

Ну что ж, теперь, когда объект умеет однозначно определить свое состояние, давайте посмотрим, какие возможности это перед нами открывает:

  • Запись состояния в хранилище
  • Чтение и восстановление состояния из хранилища
  • Проверка на эквивалентность своего состояния с состоянием другого объекта
  • Копирование состояния другому объекту
  • Клонирование объекта — создание нового объекта того же типа с идентичным состоянием.

С первыми двумя пунктами все должно быть ясно — речь об этом уже шла выше. Объект умеет работать с интерфейсом дерева состояния, а предоставлять это дерево может любой класс, реализующий физическое хранилище.

По поводу третьего пункта — в классе TMyPersistent мы можем реализовать метод Equals, в котором получать состояния сравниваемых объектов и проверять их на эквивалентность.

Следующий пункт — копирование состояния другому объекту. В классе TMyPersistent мы можем реализовать методы Assign и AssignTo, но в отличие от своих тезок в классе TPersistent, которые не делают ничего полезного, мы можем полностью скопировать состояние объекта источника объекту приемнику. Для этого можно сохранить состояние объекта источника в дерево, а приемником произвести загрузку из этого дерева.

Для клонирования же нужно создать сначала новый объект того же типа, а потом скопировать ему состояние исходного объекта.

КРИТИКА ПРАКТИЧЕСКОГО РАЗУМА

В прилагаемом к статье архиве Вы найдете библиотеку, в которой кроме прочего реализован класс TSyPersistent (юнит SyClasses.pas). В этом классе реализованы описанные чуть выше идеи. Ключевыми являются методы SaveToTree/LoadFromTree, которые работают с RTTI. Остальные методы просто их используют (кроме CustomSerialize/CustomDeSerialize, о которых поговорим ниже). Реализацию методов в тексте статьи приводить не буду, при желании вы можете ознакомиться с ней самостоятельно.

{$M+}
  // Базовый класс перманентных объектов
  TSyPersistent = class(TSyObject)
  ...
  protected
    procedure AssignTo(Dest: TSyPersistent); virtual;
    procedure CustomSerialize(const Node: ISyTreeNode); virtual;
    procedure CustomDeSerialize(const Node: ISyTreeNode); virtual;
  public
    constructor Create; virtual;
    procedure SaveToTree(const Node: ISyTreeNode);
    procedure LoadFromTree(const Node: ISyTreeNode);
    procedure Assign(Source: TSyPersistent); virtual;
    function Equals(AObject: TSyPersistent): Boolean; virtual;
    function Clone: TSyPersistent;
    ...
  end;
{$M+}

Вы можете просто порождать от этого класса потомков, которые автоматически смогут записывать свое состояние в дерево, восстанавливать из дерева, проверять экземпляры на эквивалентность и копировать состояние другим экземплярам.

Интерфейс абстрактного дерева состояния представляется типом ISyTreeNode. Вот его объявление:

// Интерфейс узла дерева
ISyTreeNode = interface(IInterface)
['{6934A38E-3605-4A8E-A120-D7C94C5C169C}']
  // Имя узла
  function GetName: String;
  procedure SetName(const Value: String);
  property Name: String read GetName write SetName;
  // Родитель
  function GetParent: ISyTreeNode;
  property Parent: ISyTreeNode read GetParent;
  // Список дочерних подузлов
  function GetNodesCount: Integer;
  function GetNode(Index: Integer): ISyTreeNode;
  function GetNodeByName(AName: String): ISyTreeNode;
  function GetNodeIndex(AName: String): Integer;
  property NodesCount: Integer read GetNodesCount;
  property Nodes[Index: Integer]: ISyTreeNode read GetNode;
  property NodeByName[AName: String]: ISyTreeNode read GetNodeByName;
  property NodeIndex[AName: String]: Integer read GetNodeIndex;
  // Список параметров узла
  function GetParamsCount: Integer;
  function GetParamName(Index: Integer): String;
  function GetParamValue(Index: Integer): Variant;
  procedure SetParamValue(Index: Integer; const Value: Variant);
  function GetParamValueByName(AName: String): Variant;
  procedure SetParamValueByName(AName: String; const Value: Variant);
  function GetParamIndex(AName: String): Integer;
  property ParamsCount: Integer read GetParamsCount;
  property ParamName[Index: Integer]: String read GetParamName;
  property ParamValue[Index: Integer]: Variant read GetParamValue write
    SetParamValue;
  property ParamValueByName[AName: String]: Variant read GetParamValueByName
    write SetParamValueByName;
  property ParamIndex[AName: String]: Integer read GetParamIndex;
  // Управление узлами
  function AddNode(AName: String): ISyTreeNode;
  procedure DeleteNode(Index: Integer); overload;
  procedure DeleteNode(AName: String); overload;
  procedure DeleteNode(Node: ISyTreeNode); overload;
  procedure ClearNodes;
  function NodeExists(AName: String): Boolean;
  procedure Assign(const Node: ISyTreeNode);
  // Управление параметрами
  procedure AddParam(AName: String; AValue: Variant);
  procedure AddBinaryParam(AName: String; Ptr: Pointer; Size: Integer;
    ByRef: Boolean);
  procedure DeleteParam(Index: Integer); overload;
  procedure DeleteParam(AName: String); overload;
  procedure ClearParams;
  function ParamExists(AName: String): Boolean;
  // Полная очистка
  procedure Clear;
end;

Выглядит громоздко, но ничего сложного, на мой взгляд, в нем нет. Каждый узел дерева содержит два списка: список параметров, у которых есть имя (String) и значение (Variant), а также список дочерних узлов. Соответственно методы этого интерфейса позволяют управлять этими узлами и параметрами.

В примерах далее мы будем рассматривать только сериализацию объектов в различные хранилища, так как именно на примере сериализации легче всего увидеть, что именно объект определяет как свое состояние. Учитывая, что все остальные методы работают именно с этим состоянием (если их не перекрыть), то их использование и поведение становится очевидным.

Простые классы

Приведем пример простого класса, способного определять свое состояние в виде опубликованных свойств.

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

Если нам нужно записать состояние этого объекта в XML-файл, то это будет выглядеть примерно так:

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

В данном примере мы воспользовались классом TSyXmlFile из модуля SyDocuments.pas. Этот класс предоставляет интерфейс дерева через свойство RootNode и позволяет сохранять состояние в формате XML. Полученный в результате файл будет выглядеть так:

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

Вместо класса TSyXmlFile можно было бы использовать, например, класс TSyIniFile уже реализованный в библиотеке (модуль SyDocuments.pas), для сохранения данных в формате ini-файла, или любой свой класс, предоставляющий интерфейс дерева состояния.

Вложенные объекты

Если опубликованное свойство имеет классовый тип, унаследованный от TSyPersistent, то при записи этого свойства в дерево, оно образует вложенный узел. Таким образом, мы можем делать наши перманентные объекты составными.

Рассмотрим еще один пример реализации класса настроек приложения, в котором сгруппируем параметры, управляющие показом сплеш-окна, в отдельный класс:

uses SyClasses;
...
// Параметры показа сплеш-окна
TSplashParams = class(TSyPersistent)
private
  FSplashTimeout: Integer;
  FShowSplash: Boolean;
public
  constructor Create; override;
published
  property ShowSplash: Boolean read FShowSplash write FShowSplash;
  property SplashTimeout: Integer read FSplashTimeout write FSplashTimeout;
end;

// Настройки приложения
TApplicationSettings = class(TSyPersistent)
private
  FUserProfile: String;
  FSplashParams: TSplashParams;
  procedure SetSplashParams(const Value: TSplashParams);
public
  constructor Create; override;
  destructor Destroy; override;
published
  property UserProfile: String read FUserProfile write FUserProfile;
  property SplashParams: TSplashParams read FSplashParams write SetSplashParams;
end;
...
{ TSplashParams }

constructor TSplashParams.Create;
begin
  inherited Create;
  FShowSplash := True;
  FSplashTimeout := 2000;
end;

{ TApplicationSettings }

constructor TApplicationSettings.Create;
begin
  inherited Create;
  FSplashParams := TSplashParams.Create;
end;

destructor TApplicationSettings.Destroy; 
begin
  FSplashParams.Free;
  inherited Destroy;
end;

procedure TApplicationSettings.SetSplashParams(const Value: TSplashParams);
begin
  FSplashParams.Assign(Value);
end;

Для разнообразия покажем, как данный класс можно записать в ini-файл:

uses SyDocuments;
...
var
  Ini: TSyIniFile;
  Settings: TApplicationSettings;
begin
  Settings := TApplicationSettings.Create;
  // Создаем ini-файл с "корневой" секцией 'CommonSettings'
  Ini := TSyIniFile.Create('Settings.ini', 'CommonSettings');
  try
    Settings.UserProfile := 'user.dat';
    // Записываем настройки в корневой узел дерева
    Settings.SaveToTree(Ini.RootNode);
  finally
    Ini.Free;
    Settings.Free;
  end;
end;

Полученный файл будет выглядеть следующим образом:

[CommonSettings]
UserProfile=user.dat
[SplashParams]
ShowSplash=True
SplashTimeout=2000

Если бы мы сохранили такой объект в XML с использованием класса TSyXmlFile, получили бы примерно такой файл:

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

Настраиваемая сериализация

Как уже было сказано выше, не всегда состояние объекта можно описать только с помощью опубликованных свойств. Иногда удобно определять дополнительные параметры, характеризующие состояние объекта.

Как видно из объявления класса TSyPersistent, у него имеются виртуальные методы CustomSerialize и CustomDeSerialize. Эти методы нельзя вызывать напрямую, однако они вызываются автоматически внутри SaveToTree и LoadFromTree соответственно. Параметром этим методам передается узел дерева, куда/откуда производится запись/чтение. В базовом классе эти методы ничего не делают, однако мы можем перекрыть их в потомках. В реализации этих методов необходимо самостоятельно работать с интерфейсом абстрактного дерева ISyTreeNode.

В качестве примера применения настраиваемой сериализации, приведем класс, являющийся надстройкой над TStrings, однако являющегося потомком TSyPersistent. Это означает, что для данного класса будут работать все перечисленные выше методы сериализации, копирования состояния, проверки эквивалентности и клонирования, а также свойство такого типа можно будет объявить опубликованным у другого потомка TSyPersistent, в результате чего, последний будет считать список строк частью своего состояния.

uses Classes, SyClasses;

TSyStringsAdapter = class(TSyPersistent)
private
  FStrings: TStrings;
protected
  procedure CustomSerialize(const Node: ISyTreeNode); override;
  procedure CustomDeSerialize(const Node: ISyTreeNode); override;
public
  constructor Create; override;
  destructor Destroy; override;
  // Свойство, открывающее доступ к списку строк
  property Strings: TStrings read FStrings write SetStrings;
end;
...
constructor TSyStringsAdapter.Create;
begin
  inherited Create;
  FStrings := TStringList.Create; // Создаем внутренний список, хранящий строки
end;

procedure TSyStringsAdapter.CustomSerialize(const Node: ISyTreeNode);
var
  i: Integer;
begin
  // Сохраняем каждую строку списка в дерево в виде параметра
  for i := 0 to FStrings.Count - 1 do
    Node.AddParam('item', FStrings[i]);
end;

procedure TSyStringsAdapter.CustomDeSerialize(const Node: ISyTreeNode);
var
  i: Integer;
begin
  FStrings.BeginUpdate;
  try
    FStrings.Clear;  // Очистка списка перед восстановлением его состояния
    // Читаем из дерева все параметры и заносим их значения в список
    for i := 0 to Node.ParamsCount - 1 do
      if Node.ParamName[i] = 'item' then // На всякий случай
        FStrings.Add(Node.ParamValue[i]);
  finally
    FStrings.EndUpdate;
  end;
end;

destructor TSyStringsAdapter.Destroy;
begin
  FStrings.Free;
  inherited Destroy;
end;

procedure TSyStringsAdapter.SetStrings(Value: TStrings);
begin
  FStrings.Assign(Value);
end;

Несмотря на то, что класс TSyStringsAdapter не имеет опубликованных свойств, его состояния не будет пустым, так как мы вручную определили параметры объекта и формат их записи. Если бы класс имел опубликованные свойства, они также были бы записаны, так как настраиваемая сериализация не отменяет запись свойств, а дополняет ее.

Теперь добавим в класс настроек приложения свойство RecentProjects, которое якобы будет хранить список последних открываемых в нашей программе проектов:

uses SyClasses;
type
  TApplicationSettings = class(TSyPersistent)
  private
    FUserProfile: String;
    FSplashParams: TSplashParams;
    FRecentProjects: TSyStringsAdapter;
    procedure SetSplashParams(const Value: TSplashParams);
    procedure SetRecentProjects(const Value: TSyStringsAdapter);
  public
    constructor Create; override;
    destructor Destroy; override;
  published
    property UserProfile: String read FUserProfile write FUserProfile;
    property SplashParams: TSplashParams read FSplashParams write
      SetSplashParams;
    property RecentProjects: TSyStringsAdapter read FRecentProjects 
      write SetRecentProjects;
  end;
...
constructor TApplicationSettings.Create;
begin
  inherited Create;
  FSplashParams := TSplashParams.Create;
  FRecentProjects := TSyStringsAdapter.Create;
end;

destructor TApplicationSettings.Destroy; 
begin
  FRecentProjects.Free;
  FSplashParams.Free;
  inherited Destroy;
end;

Создадим экземпляр такого класса, заполним его тестовыми параметрами и произведем запись в XML-файл:

uses SyDocuments;
...
var
  Settings: TApplicationSettings;
  XmlDoc: TSyXmlFile;
begin
  Settings := TApplicationSettings.Create;
  XmlDoc := TSyXmlFile.Create('Settings.xml', 'AppticationSettings');
  try
    Settings.UserProfile := 'user.dat';
    Settings.RecentProjects.Strings.Add('MyProject1.prj');
    Settings.RecentProjects.Strings.Add('MyProject2.prj');
    // Записываем состояние объекта в файл 'Settings.xml'
    Settings.SaveToTree(XmlDoc.RootNode);
  finally
    XmlDoc.Free;
    Settings.Free;
  end;
end;

Файл получился таким:

<?xml version="1.0" encoding="windows-1251"?>
<ApplicationSettings>
  <SplashParams>
    <ShowSplash>True</ShowSplash>
    <SplashTimeout>2000</SplashTimeout>
  </SplashParams>
  <RecentProjects>
    <item>MyProject1.prj</item>
    <item>MyProject2.prj</item>
  </RecentProjects>
  <UserProfile>user.dat</UserProfile>
</ApplicationSettings>

Класс TSyCollection

Класс TSyCollection является контейнером объектов-потомков TSyPersistent, и сам в свою очередь является прямым потомком данного класса, что позволяет объявлять опубликованные свойства типа TSyCollection, которые будут записаны в поток.

TSyCollection = class(TSyPersistent)
  ...
  protected
    procedure CustomSerialize(const Node: ISyTreeNode); override;
    procedure CustomDeSerialize(const Node: ISyTreeNode); override;
  public
    constructor Create; override;
    destructor Destroy; override;
    // Методы управления содержимым коллекции
    function Add(AObject: TSyPersistent): Integer;
    procedure Delete(Index: Integer);
    procedure Clear;
    // Свойства
    property Items[Index: Integer]: TSyPersistent read Get write Put; default;
    property Count: Integer read FCount;
    property Capacity: Integer read GetCapacity write SetCapacity;
  end;

Для управления содержимым коллекции доступны методы Add, Delete и Clear. Доступ к элементам осуществляется через индексированное свойство Items.

При записи коллекции в поток записываются состояния всех ее элементов, а также их классы, что позволяет при чтении коллекции полностью воссоздать ее объекты.

Вот некоторые особенности класса TSyCollection:

  • Коллекция может содержать разнотипные элементы.
  • Коллекция владеет своими элементами — это значит, что при добавлении объекта в коллекцию, заботиться о его уничтожении не нужно. Объект будет уничтожен автоматически при удалении из коллекции или при уничтожении последней.
  • Классы объектов, помещаемых в коллекцию, должны быть зарегистрированы под некоторым строковым именем в специальном реестре классов с помощью процедуры SyClasses.RegisterClass. Это необходимо из-за того, что коллекция может содержать разнотипные элементы, а следовательно, при записи элемента необходимо каким-либо образом записать его тип, и наоборот — при чтении коллекции по строковому идентификатору типа создать соответствующий данному типу элемент. Регистрацию необходимо произвести до первого чтения/записи коллекции. Рекомендуется делать это в секции initialization того модуля, в котором классы-элементы объявлены.

Приведем пример работы с классом TSyCollection на примере контейнера геометрических фигур:

uses SyClasses;

type
  // Класс, описывающий окружность
  TCircle = class(TSyPersistent)
  public
    constructor Create(AX, AY, AR: Integer);
  published
    property X: Integer read FX write FX;
    property Y: Integer read FY write FX;
    property R: Integer read FR write FR;
  end;

  // Класс, описывающий прямоугольник
  TRectangle = class(TSyPersistent)
  public
    constructor Create(ALeft, ATop, ARight, ABottom: Integer);
  published
    property Left: Integer read FLeft write FLeft;
    property Top: Integer read FTop write FTop;
    property Right: Integer read FRight write FRight;
    property Bottom: Integer read FBottom write FBottom;
  end;

...

initialization
  // Регистрируем классы-элементы коллекции
  SyClasses.RegisterClass(TCircle, 'Circle');
  SyClasses.RegisterClass(TRectangle, 'Rectangle');

end;

Теперь создадим коллекцию, заполним ее объектами и сохраним в файл:

uses SyDocuments;
...
var
  Items: TSyCollection;
  XmlDoc: TSyXmlFile;
begin
  // Создаем коллекцию
  Items := TSyCollection.Create;
  XmlDoc := TSyXmlFile.Create('Shapes.xml', 'Shapes');
  try
    // Заполняем ее элементами
    Items.Add(TCircle.Create(15, 20, 10));
    Items.Add(TRectangle.Create(12, 12, 20, 25));
    Items.Add(TCircle.Create(5, 10, 8));
    // Сохраняем коллекцию в файл
    Items.SaveToTree(XmlDoc.RootNode);
  finally
    XmlDoc.Free;
    Items.Free;
  end;
end;

Полученный файл выглядит следующим образом:

<?xml version="1.0" encoding="windows-1251"?>
<Shapes>
  <Circle>
    <X>15</X>
    <Y>20</Y>
    <R>10</R>
  </Circle>
  <Rectangle>
    <Left>12</Left>
    <Top>12</Top>
    <Right>20</Right>
    <Bottom>25</Bottom>
  </Rectangle>
  <Circle>
    <X>5</X>
    <Y>10</Y>
    <R>8</R>
  </Circle>
</Shapes>

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

Управление состоянием

Бывают случаи, когда для различных операций состояние объекта должны отражать различные сущности. Представим, что у нас имеется некий класс, содержащий свойство Description: String, которое должно записываться в поток при сериализации объекта, но не должно учитываться при сравнении объектов на эквивалентность. Для решения данной задачи можно воспользоваться директивой stored, которая позволяет во время выполнения делать вывод о том, является ли свойство частью состояния объекта на данный момент или нет. После этой директивы можно указать на функцию, возвращающую Boolean, которая будет вызвана перед записью свойства. Если функция вернет False, свойство записано не будет.

Поставленная задача сводится к написанию такой функции, которая вернет False в случае если на данный момент объект сравнивается с другим. Сделать это можно, используя свойство State класса TSyPersistent.

type
  TSyPersistState = set of (psSaving, psLoading, psAssigning, psAssigned,
    psComparing);

  TSyPersistent = class(TSyObject)
  ...
  protected
    ...
    property State: TSyPersistState read FState write FState;
  end;

Свойство представляет собой множество, в которое могут входить следующие значения:

  • psSaving — устанавливается на время записи состояния объекта в дерево.
  • psLoading — устанавливается на время чтения состояния объекта из дерева.
  • psAssigning — устанавливается на время, когда объекту копируется состояние другого объекта.
  • psAssigned — устанавливается на время, когда объект копирует свое состояние другому объекту.
  • psComparing — устанавливается объектам на время сравнения на эквивалентность.

Воспользуемся этим свойством:

type
  TSomeObject = class(TSyPersistent)
  private
    FDescription: Boolean;
    function IsNotComparing: Boolean;   
  published
    // Свойство разрешается записывать в дерево состояния только в том случае,
    // если в данный момент не идет сравнение на эквивалентность
    property Description: String read FDescription write FDescription stored 
      IsNotComparing;
  end;
...

function TSomeObject.IsNotComparing: Boolean;
begin
  Result := not (psComparing in State);
end;

Перед записью свойства в дерево состояния сначала будет автоматически вызван метод IsNotComparing, и если он вернет False, то свойство записано не будет. А произойдет это только в том случае, если запись состояния в дерево было инициировано вызовом метода Equals.

Ограничения

В заключение, хотелось бы рассказать о некоторых ограничениях, которые имеет как сам продемонстрированный подход в целом, так и данная конкретная его реализация.

  • Быстродействие. Следует иметь в виду, что цена, которую мы платим при получении возможности подобным образом оперировать над состоянием объекта — это снижение быстродействия. Код, выполняющий те же действия по сериализации, копирования или проверки эквивалентности, но делающий это непосредственно, а не обращающийся к RTTI, и тем более, не использующий всякие промежуточные сущности, вроде дерева состояния, всегда будет работать быстрее. Однако понятно, что в этом случае мы потеряем возможность написать такой код раз и навсегда. Чаще всего потери по быстродействию не критичны, но если скорость выполнения при использовании описанного подхода не устраивает, то от него необходимо отказаться.
  • Так как класс TSyPersistent не является потомком TPersistent, то приведенные принципы не совместимы с используемыми для классов TPersistent и TComponent. Вы не сможете комбинировать эти подходы в рамках одного класса, однако частично совместить их можно с помощью классов-адаптеров, один из возможных примеров которого был приведен в статье (TSyStringsAdapter).
  • В данной версии библиотеки не все типы свойств, которые могут быть объявлены опубликованными, поддерживаются. В частности, классовые свойства будут записаны в дерево только в том случае, если тип свойства является потомком TSyPersistent, а свойства-методы, которые, как правило, используются для назначения обработчиков событий, не учитываются при формировании дерева состояния вовсе. Мне показалось, что раз речь не идет о визуальном проектировании, то свойства-методы не нужны вовсе.

Заключение

Данная статья задумывалась как своего рода замена более ранней моей статьи — "Упрощаем работу с потоками". С течением времени у меня сформировался несколько другой взгляд на поставленную проблему, который в данной статье я и постарался изложить. Основная идея — высокая степень гибкости и абстрагирования.

Также хотел бы высказать слова искренней благодарности людям, помогавшим мне в подготовке данного материала — это Роман Игнатьев (Romkin) и Александр Шабля (Shabal). Большое Вам Спасибо за критику и поддержку!

Прилагаемые файлы




Смотрите также материалы по темам:
[Классы] [Запись компонент в поток и загрузка из потока.] [Работа с потоками (TStream)]

 Обсуждение материала [ 13-10-2008 11:14 ] 30 сообщений
  
Время на сайте: GMT минус 5 часов

Если вы заметили орфографическую ошибку на этой странице, просто выделите ошибку мышью и нажмите Ctrl+Enter.
Функция может не работать в некоторых версиях броузеров.

Web hosting for this web site provided by DotNetPark (ASP.NET, SharePoint, MS SQL hosting)  
Software for IIS, Hyper-V, MS SQL. Tools for Windows server administrators. Server migration utilities  

 
© При использовании любых материалов «Королевства Delphi» необходимо указывать источник информации. Перепечатка авторских статей возможна только при согласии всех авторов и администрации сайта.
Все используемые на сайте торговые марки являются собственностью их производителей.

Яндекс цитирования