Social Icons

пятница, 11 сентября 2009 г.

Гибкий маршалинг/демаршалинг в Delphi 2010

Оригинал: Custom Marshalling/UnMarshalling in Delphi 2010. Автор: Daniele Teti.

Введение
Несколько дней назад Embarcadero представила новую версию RAD Studio, 2010.
Появилось много новых возможностей, и вы можете найти массу описаний в Интернете, поэтому я не буду их повторять.

Одна из новинок, о которой последние несколько лет повсеместно просили Delphi-программисты по всему миру, включая меня - это конечно новый и более мощный RTTI.

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

Маршалинг определяется следующим образом (прим. пер.: далее идет фрагмент из русского варианта Википедии, а не прямой перевод):
"В информатике маршалинг (от англ. marshal — упорядочивать) (по смыслу похоже на сериализацию) — процесс преобразования представления объекта в памяти в формат данных, пригодный для хранения или передачи. Обычно применяется когда данные необходимо передавать между различными частями одной программы или от одной программы к другой.
Противоположный процесс называется демаршалингом (также называемый десериализацией)."
http://ru.wikipedia.org/wiki/Маршалинг

В Delphi 2010 процесс сериализации и десериализации обрабатывается соответсвенно Маршалером and Демаршалером.

Для любого объекта в Delphi встроенным форматом для сериализации является JSON.
За сериализацию объектов в JSON отвечают 2 основных класса, представленные в модуле DBXJSONReflect:
- TJSONMarshal
- TJSONUnMarshal

Скажем, у вас есть объект, объявленный так:
type
TKid = class
FirstName: String;
LastName: String;
Age: Integer;
end;
Чтобы сериализовать и десериализовать экземпляр TKid необходимо сделать следующее:
var
Mar: TJSONMarshal;  // Сериализатор   
UnMar: TJSONUnMarshal;  // Десериализатор
Kid: TKid;  // Объект для сериализации   
SerializedKid: TJSONObject;  // Сериализация для объекта   
begin   
Mar := TJSONMarshal.Create(TJSONConverter.Create);   
try   
Kid := TKid.Create; 
try
Kid.FirstName := 'Daniele';
Kid.LastName := 'Teti';      
Kid.Age := 29;      
SerializedKid := Mar.Marshal(Kid) as TJSONObject;
finally
FreeAndNil(Kid);
end;
finally
Mar.Free;
end;
// Для Kid-объекта выводим версию JSON
WriteLn(SerializedKid.ToString);  
// Демаршалируем объект
UnMar := TJSONUnMarshal.Create;
try
Kid := UnMar.UnMarshal(SerializedKid) as TKid;
try
// теперь Kid-объекта такой, каким был до маршалинга
Assert(Kid.FirstName = 'Daniele');
Assert(Kid.LastName = 'Teti');
Assert(Kid.Age = 29);
finally
Kid.Free;
end;
finally
UnMar.Free;
end;
end;
Просто, да?
Для доступа к строковому представлению JSON (нашему объекту), надо вызвать метод ToString.
JSON-представление объекта SerializedKid может быть сохранено в файл, отправлено на удаленный сервер, использовано веб-сервером на веб-странице, сохранено в БД или запущено в космос (!!!).
Когда Delphi-приложение считает JSON-строку, вы сможете воссоздать объект в таком виде, в каком он был во время сериализации.
Но при этом, каждый, имеющий JSON-парсер может прочитать данные об объекте, даже не Delphi-клиент.

Рассмотрим преимущества использования открытого формата и стандарта.

До сих пор все было просто...
Как сериализовать нестандартное поле?

Представьте, что мы добавили дату рождения к нашему TKid:
type  
TKid = class   
FirstName: String;   
LastName: String;   
Age: Integer;   
BornDate: TDateTime;   
end;
Надо сериализовать TDateTime, которая локализуется и содержится в JSON-строке как число с плавающей точкой, т.к. для Delphi TDateTime - это десятичная дробь.
Если я загружу данные из другой Delphi-программы, нет проблем, но а если я захочу прочесть скрипт в JavaScript? или .NET? или Ruby?
В таком случае для чтения я использую "DATA"-формат, в том числе для этих языков.

Новый механизм предполагает и такую сериализацию.
Необходимо, однако, сообщить Маршалеру и Демаршалеру в каком виде представлять и восстанавливать конкретное поле объекта следующими 2 операторами, например:
// Маршалер   
Marshaller.RegisterConverter(TKid, 'BornDate',   
function(Data: TObject; Field: string): string   
var   
ctx: TRttiContext; date : TDateTime;   
begin   
date := ctx.GetType(Data.ClassType).GetField(Field).GetValue(Data).AsType(TDateTime);   
Result := FormatDateTime('yyyy-mm-dd hh:nn:ss', date);   
end; 

// Демаршалер
UnMarshaller.RegisterReverter(TKid, 'BornDate',
procedure(Data: TObject; Field: string; Arg: string)
var
ctx: TRttiContext;
datetime:TDateTime;
begin
datetime := EncodeDateTime(StrToInt(Copy(Arg, 1, 4)),
StrToInt(Copy(Arg, 6, 2)),
StrToInt(Copy(Arg, 9, 2)),
StrToInt(Copy(Arg, 12, 2)),
StrToInt(Copy(Arg, 15, 2)),
StrToInt(Copy(Arg, 18, 2)), 0);
ctx.GetType(Data.ClassType).GetField(Field).SetValue(Data, datetime);
end;
Когда Маршалер сериализует поле "BornDate" вызывается анонимный метод - "Конвертер", в то же время, другой анонимный метод вызывается в Демаршалере при попытке восстановить объект из JSON-строки - "Ревертер".

Таким образом сериализация TKid гарантирует, что мой объект будет читаться и из Delphi, и с помощью другого ЯП без потери информации.

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

Представьте, мы добавили в TKid следующее:
type
TTeenager = class(TKid)
Phones: TStringList;
constructor Create; virtual;
destructor Destroy; virtual;
end;
Мы должны определить конвертер и ревертер для класса TStringList.
Мы может сделать это следующим образом:
var
Marshaller: TJSONMarshal;
UnMarshaller: TJSONUnMarshal;
Teenager: TTeenager;
Value, JSONTeenager: TJSONObject;

begin
Marshaller := TJSONMarshal.Create(TJSONConverter.Create);
try
Marshaller.RegisterConverter(TTeenager, 'BornDate',
function(Data: TObject; Field: string): string
var
ctx: TRttiContext; date : TDateTime;
begin
date := ctx.GetType(Data.ClassType).GetField(Field).GetValue(Data).AsType(TDateTime);
Result := FormatDateTime('yyyy-mm-dd hh:nn:ss', date);
end;

Marshaller.RegisterConverter(TStringList, function(Data: TObject): TListOfStrings
var
i, count: integer;
begin
count := TStringList(Data).count;
SetLength(Result, count);
for i := 0 to count - 1 do
Result[i] := TStringList(Data)[i];
end;  // Конвертер для TStringList

Teenager := TTeenager.CreateAndInitialize;
try
Value := Marshaller.Marshal(Teenager) as TJSONObject;
finally
Teenager.Free;
end;

finally
Marshaller.Free;
end;

// Демаршализация класса Teenager
UnMarshaller := TJSONUnMarshal.Create;
try
UnMarshaller.RegisterReverter(TTeenager, 'BornDate',
procedure(Data: TObject; Field: string; Arg: string)
var
ctx: TRttiContext;
datetime: TDateTime;
begin
datetime := EncodeDateTime(StrToInt(Copy(Arg, 1, 4)),
StrToInt(Copy(Arg, 6, 2)),
StrToInt(Copy(Arg, 9, 2)),
StrToInt(Copy(Arg, 12, 2)),
StrToInt(Copy(Arg, 15, 2)),
StrToInt(Copy(Arg, 18, 2)), 0);

ctx.GetType(Data.ClassType).GetField(Field).SetValue(Data, datetime);
end;

UnMarshaller.RegisterReverter(TStringList, function(Data: TListOfStrings): TObject
var
StrList: TStringList;
Str: string;
begin
StrList := TStringList.Create;
for Str in Data do
StrList.Add(Str);
Result := StrList;
end;  // Ревертер для TStringList

Teenager := UnMarshaller.Unmarshal(Value) as TTeenager;
try
Assert('Daniele' = Teenager.FirstName);
Assert('Teti' = Teenager.LastName);
Assert(29 = Teenager.Age);
Assert(EncodeDate(1979, 11, 4) = Teenager.BornDate);
Assert(3 = Teenager.Phones.Count);
Assert('NUMBER01'=Teenager.Phones[0]);
Assert('NUMBER02'=Teenager.Phones[1]);
Assert('NUMBER03'=Teenager.Phones[2]);
finally
Teenager.Free;
end;

finally
UnMarshaller.Free;
end;

end;
Существуют различные типы конвертеров и ревертеров.
В модуле DBXJSONReflect есть 8 типов конвертеров:
// Преобразует поле в массив объектов
TObjectsConverter = reference to function(Data: TObject; Field: String): TListOfObjects;

// Преобразует поле в строковый массив
TStringsConverter = reference to function(Data: TObject; Field: string): TListOfStrings;

// Преобразует тип в массив объектов
TTypeObjectsConverter = reference to function(Data: TObject): TListOfObjects;

// Преобразует тип в строковый массив
TTypeStringsConverter = reference to function(Data: TObject): TListOfStrings;

// Преобразует поле в объект
TObjectConverter = reference to function(Data: TObject; Field: String): TObject;

// Преобразует поле в строку
TStringConverter = reference to function(Data: TObject; Field: string): string;

// Преобразует предопределенный тип в объект
TTypeObjectConverter = reference to function(Data: TObject): TObject;

// Преобразует предопределенный тип в строку
TTypeStringConverter = reference to function(Data: TObject): string;
Каждый из них работает с конкретным представлением объекта в конечной сериализации, в нашем случае мы будем использовать их преобразования в JSON.
Также в модуле DBXJSONReflect определено множество "Ревертеров", работающих с сериализованным видом данных и использующих их для восстановления прежде сериализованного объекта.
Т.к. как они дополняют Конвертеры, я не буду здесь их приводить.

В качестве последнего примера создадим класс TProgrammer из TTeenager, добавив список лаптопов и их свойства.
В связи с этим необходимо объявить новую пару Конвертер/Ревертер.

В этом примере я объявил конвертер и ревертер в другом модуле для получение более читабельного кода:
type
TLaptop = class
Model: String;
Price: Currency;
constructor Create(AModel: String; APrice: Currency);
end;

TLaptops = TObjectList(TLaptop);
TProgrammer = class(TTeenager)
Laptops: TLaptops;
constructor Create; override;
destructor Destroy; override;
class function CreateAndInitialize: TProgrammer;
end;

// Код реализации...
var
Marshaller: TJSONMarshal;
UnMarshaller: TJSONUnMarshal;
Programmer: TProgrammer;
Value, JSONProgrammer: TJSONObject;
begin
Marshaller := TJSONMarshal.Create(TJSONConverter.Create);
try
Marshaller.RegisterConverter(TProgrammer, 'BornDate', ISODateTimeConverter);
Marshaller.RegisterConverter(TStringList, StringListConverter);
Marshaller.RegisterConverter(TProgrammer, 'Laptops', LaptopListConverter);
Programmer := TProgrammer.CreateAndInitialize;
try
Value := Marshaller.Marshal(Programmer) as TJSONObject;
finally
Programmer.Free;
end;

// Демаршалируем класс TProgrammer
UnMarshaller := TJSONUnMarshal.Create;
try
UnMarshaller.RegisterReverter(TProgrammer, 'BornDate', ISODateTimeReverter);
UnMarshaller.RegisterReverter(TStringList, StringListReverter);
UnMarshaller.RegisterReverter(TProgrammer, 'Laptops', LaptopListReverter);
Programmer := UnMarshaller.Unmarshal(Value) as TProgrammer;
try
Assert('Daniele' = Programmer.FirstName);
Assert('Teti' = Programmer.LastName);
Assert(29 = Programmer.Age);
Assert(EncodeDate(1979, 11, 4) = Programmer.BornDate);
Assert(3 = Programmer.Phones.Count);
Assert('NUMBER01' = Programmer.Phones[0]);
Assert('NUMBER02' = Programmer.Phones[1]);
Assert('NUMBER03' = Programmer.Phones[2]);
Assert('HP Presario C700' = Programmer.Laptops[0].Model);
Assert(1000 = Programmer.Laptops[0].Price);
Assert('Toshiba Satellite Pro' = Programmer.Laptops[1].Model);
Assert(800 = Programmer.Laptops[1].Price);
Assert('IBM Travelmate 500' = Programmer.Laptops[2].Model);
Assert(1300 = Programmer.Laptops[2].Price);
finally
Programmer.Free;
end;
finally
UnMarshaller.Free;
end;
finally
Marshaller.Free;
end; 
end;
Модуль CustomConverter.pas содержит все необходимые Конвертеры/Ревертеры как анонимные методы.
unit CustomConverter;

interface

uses
DBXJSONReflect,
MyObjects; //Needed by converter and reverter for TLaptops

var
ISODateTimeConverter: TStringConverter;
ISODateTimeReverter: TStringReverter;
StringListConverter: TTypeStringsConverter;
StringListReverter: TTypeStringsReverter;
LaptopListConverter: TObjectsConverter;
LaptopListReverter: TObjectsReverter;

implementation

uses
SysUtils, RTTI, DateUtils, Classes;

initialization

LaptopListConverter := function(Data: TObject; Field: String): TListOfObjects

var
Laptops: TLaptops;
i: integer;
begin
Laptops := TProgrammer(Data).Laptops;
SetLength(Result, Laptops.Count);
if Laptops.Count ) 0 then
for I := 0 to Laptops.Count - 1 do
Result[I] := Laptops[i];
end;

LaptopListReverter := procedure(Data: TObject; Field: String; Args: TListOfObjects)
var
obj: TObject;
Laptops: TLaptops;
Laptop: TLaptop;
i: integer;
begin
Laptops := TProgrammer(Data).Laptops;
Laptops.Clear;
for obj in Args do
begin
laptop := obj as TLaptop;
Laptops.Add(TLaptop.Create(laptop.Model, laptop.Price));
end;
end;

StringListConverter := function(Data: TObject): TListOfStrings
var
i, count: integer;
begin
count := TStringList(Data).count;
SetLength(Result, count);
for i := 0 to count - 1 do
Result[i] := TStringList(Data)[i];
end;


StringListReverter := function(Data: TListOfStrings): TObject
var
StrList: TStringList;
Str: string;
begin
StrList := TStringList.Create;
for Str in Data do
StrList.Add(Str);
Result := StrList;
end;

ISODateTimeConverter := function(Data: TObject; Field: string): string
var
ctx: TRttiContext; date : TDateTime;
begin
date := ctx.GetType(Data.ClassType).GetField(Field).GetValue(Data).AsType(TDateTime);
Result := FormatDateTime('yyyy-mm-dd hh:nn:ss', date);
end;

ISODateTimeReverter := procedure(Data: TObject; Field: string; Arg: string)
var
ctx: TRttiContext;
datetime: TDateTime;
begin
datetime := EncodeDateTime(StrToInt(Copy(Arg, 1, 4)), StrToInt(Copy(Arg, 6, 2)), StrToInt(Copy(Arg, 9, 2)), StrToInt(Copy(Arg, 12, 2)), StrToInt(Copy(Arg, 15, 2)), StrToInt(Copy(Arg, 18, 2)), 0);
ctx.GetType(Data.ClassType).GetField(Field).SetValue(Data, datetime);
end;

end.

Последний штрих...
Каждый процесс сериализации/десериализации может создавать предупреждения.
Такие предупреждения содержатся в свойстве "Warnings" объекта сериализации/десериализации.

Заключение
В этой статье я попытался познакомить вас с основами нового механизма сериализации в Delphi 2010.
Во время следующей конференции ITDevCon, которая пройдет в Италии 12 ноября, в своем выступлении я копну глубже по теме сериализации и RTTI.
Все интересующие и соображающие разработчики приветствуются :-)

З.Ы. для итальянцев.
Если кто-то из итальянских программистов хочет иметь версию этой статьи на итальянском языке, он может оставить комментарий ниже.

Код проекта DUnit можно скачать здесь.

1 комментарий:

  1. Спасибо за полезную статью!
    Хотелось бы высказать свои пожелания к коду насчет маршалинга/демаршалинга TDateTime. По моему мнению, перед маршалингом время нужно конвертить в UTC представление (т.е. по Гринвичу, без всяких переводов зима-лето). А при демаршалинге конвертить UTC время в ту часовую зону, в который находится комп с демаршалингом. Ибо этот комп-клинент может стоять в Китае, а сервер - в США.

    ОтветитьУдалить

Поделитесь с друзьями!

 

Подписчики

Статистика