[Из песочницы] Универсальный конвертер данных на платформе .Net Framework

В этой статье я хотел бы рассказать опыт нашей команды по созданию универсального конвертера данных. На первый взгляд звучит очень просто, что тут сложного? Взять один тип данных привести к другому типу. А если данные это структура? Тоже не трудно, вы скажете, просто нужно выполнить меппинг полей. Да, просто. Но когда целевых структур несколько, все они сложные и требуется конвертация «на лету», да еще и с обогащением данных, то как говорится «надо думать».


Перед командой была поставлена задача:
Написать конвертер данных из одной структуры в несколько других целевых структур. Причем формат хранения данных источника и данных назначения могут быть абсолютно произвольными. Конвертация должна выполняться на основе правил с возможностью повторного использования и редактирования. В процессе конвертации некоторые данные нужно перекодировать, например перевести строку »#ff0000» в строку «red».
Итак, приступим. В теории входные и выходные форматы могут быть любого типа (csv, json и т.д.). Для наглядности выберем формат XML.


Пример XML источника — «конвертировать ИЗ»:



    #ff0000
    5296 cm
    1848 cm
    31000 USD


Пример XML назначения — «конвертировать В»:



    
        
            red
        
        
            ft
            17.3753
            6.0630
        
    











    
        RUB
        1600000
    


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


  1. Цвет автомобиля Car.Color в источнике отображаются как RBG код »#ff0000», а в объекте назначения его нужно перекодировать в словесную интерпретацию «red» в тег Vehicle.Body.Exterior.Color;
  2. Длина автомобиля Car.Lenght нужно распарсить на несколько составляющих, величину измерения и единицу измерения и перевести в американские футы, получившееся значение положить в Vehicle.Size.Length;
  3. Цену автомобиля Car.Price нужно также распарсить на составляющие, пересчитать по курсу ЦБ в рублях на дату пересчета положить в Vehicle.Msrp.


Выбор контейнера для доступа к данным


Работать напрямую с XML форматом мы не можем, т.к. во первых это текст, а во вторых есть требование не привязываться к формату. В этом случае логично работать с объектами-контейнерами в памяти компьютера, которые будут иметь удобный интерфейс доступа к своим данным и иметь структурный тип для ссылки на его части.
Для этого наилучшим образом подходят обычные С# классы, у которых структура точно соответствует данным для хранения. Создание этого класса значительно упрощается если XML типизированный и в наличии есть XSD схема. С помощью утилит можно собрать класс автоматично и использовать его в коде без лишних трудозатрат.
Ниже описываются классы для наших структур


Класс-контейнер C# источника:


public class Car
{
    public string Color;
    public string Length;
    public string Width;
    public decimal Price;
}


Класс-контейнер C# назначения:


public class Vehicle {
    public Body Body;
    public Msrp Msrp;
}
public class Body
{
    public Exterior Exterior;
    public Size Size;
}
public class Msrp
{
    public string Currency;
    public decimal Value;
}
public class Exterior
{
    public string Color;
}
public class Size
{
    public string MeasureUnit;
    public decimal Length;
    public decimal Width;
}


Загрузка данных источника в контейнер


В .Net Framework есть готовые компоненты выполняющие десериализацию данных XML, с помощью которых получим экземпляр класса автоматично заполненный данными источника.
Если файл более специфичного формата, то не составит труда написать заказную библиотеку загрузки данных.


Доступ к данным контейнеров


Первое, что нам нужно научиться, это иметь единый способ доступа к данным контейнеров с произвольными структурами. Т.е. нам нужен доступ к метаданным контейнера. Это решается через рефлексию .Net. Мы можем добраться до любого свойства или поля класса, а зная тип и расположение данных мы сможем их модифицировать.
Для прямого указания структурного элемента (узла) будем использовать аналогию XPath для XML. Например, чтобы указать в источнике нужный нам узел достаточно указать строчку «Car.Color».


Правила конвертации данных контейнера-источника в контейнер-назначения


Итак, мы имеем два контейнера, оба имеют структурированную архитектуру. Теперь нам надо научиться конвертировать один в другой, из контейнера-источника в контейнер-назначение.
Как было указано в постановке задачи, конвертация должна выполняться на основе набора правил. Правила должны обладать универсальностью, чтобы их можно было использовать многократно.
В коде вырисовывается следующая схема взаимодействия (см. схему ниже): Данные сериализуются из XML в объект .Net (1–2), далее путем обращения к данным контейнера (2) происходит преобразование исходя из списка (3) правил в контейнер назначения (2–3–4). Причем правила имеют возможность обогащать данные (3–3»-3). После того как контейнер-назначение инициализирован, данные выгружаются в конечный формат (4–5).


Схема 1. Схема взаимодействия компонент внутри конвертора:


Схема взаимодействия компонент внутри конвертора

Теперь разработаем механизм конвертации с применением правил. С их помощью мы должны уметь описать любую конвертацию.
Писать новый язык правил, а потом реализовывать для него отдельный компилятор или интерпретатор — это явно лишнее. Мы решили воспользоваться обыкновенным кодом C#, который всегда можно скомпилировать и подключить к существующему функционалу. Были разработаны несколько интерфейсов и базовых классов C#.


Сам конвертер:


public interface IConverter
{
    T Convert(Object source, IDictionary rules) where T : class, new();
...
}


, где список правил это IDictionary, в котором ключи string это пути к данным контейтера-назначения, например «Vehicle.Msrp»


И правило конвертации:


public abstract class ConversionRule
{
    public abstract object GetValue(object source);
...
}


Задача конвертера, произвести преобразование указанного объекта источника source в новый объект типа T в соответствии со списком правил.
Во время конвертации «источника» в «назначение», конвертер выполняет следующие действия.


  • Создает экземпляр типа Т (тип контейнера-назначения)
  • Рекурсивно пробегает каждое поле и свойство целевого объекта типа T (контейнера-назначения), ищет для его соответствующее правила конвертации и выполняет инициализацию (присвоение значения вычисленные на основе правила). Если правила не найдено, но узел остается не заполненным.


На вход каждому правилу передается объект контейнер-источник source. Правило должно выполнить расчет и вернуть результирующее значение. Как видно на примере, в правилах конвертации нет строгой типизации, на вход может быть передан объект, на выходе мы тоже получаем объект.
Рассмотрим пример правила, которое: получает цену автомобиля Car.Price разбирает на составляющие, пересчитает по курсу ЦБ в рублях (на дату пересчета) и записывает значение в Vehicle.Msrp» целевого контейнера.


Ниже представлена таблица настройки правила конвертации:


Целевой узел в объекте назначения Правило конвертации (класс в сборке) Параметры для правила конвертации
Vehicle.Msrp ConvertStringPriceToNewStruct TargetCurrency = «RUB», SourcePath = «Car.Price»


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


public class ConvertStringPriceToMsrp: ConvertionRule
{
    public string TargetCurrency;
    public string SourcePath;
    public override object GetValue(object source)
   {
         var targetObject = new Msrp();
         targetObject.Currency = ‘TargetCurrency’;
         targetObject.Value = SplitAndCalc(GetFiled(source(), SourcePath);
         return targetObject;
   }
...
}


Перед запуском правила, выполняется его инициализация путем перебора его полей и свойств через рефлексию и заполнения одноименных значениями TargetCurrency, SourcePath из конфига (набора параметров для конкретного экземпляра правила).
Обрабатывая данное правило, объект ConvertStringPriceToMsrp берет значение поля в контейнере источнике Car.Price, разбивает строку на составляющие: цена и валюта, и создает результирующий объект Msrp, заполняя поля Msrp.Curreny = RUB и Msrp.Value=[цена в рублях].
Как видно из описания, правилу еще необходимо обратиться к внешнему источнику данных, чтобы получить текущий курс рубля к доллару. Т.е. правило конвертации может подключаться к любым внешним источникам данных и выполнять обогащение данных.


Выгрузка данных назначения из контейнера


Выгрузка данных из объекта назначения в XML выполняется так же, готовой библиотекой .Net Framework путем сериализации объекта. Компонента аккуратно сложит данные полей и свойств класса в XML структуру.


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


Действующий прототип достоинства и проблемы


Для автоматической подгрузки сервисных библиотек-справочников (для обогащения данных, для многократно используемых справочников) мы внедрили IoC Autofac. Таким образом при конвертации большого количества однородных данных мы решили проблему лишней нагрузки на ввод-вывод и ускорили обработку.


Конвертация к объекту назначения происходит в один проход без лишних циклов.
Благодаря рекурсивности, есть возможность подстановки значения узла опционально «на выбор». Данная опция весьма полезна для XML, когда структура одного тега зависит от другого (например от типа товара, заполняются разные теги — мы это активно используем при формировании XML в Amazon API).


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


Все настройки правил мы вынесли на Web интерфейс, что бы пользователи могли оперативно менять настройки. Настройки конвертаций сначала хранили в XML, но для удобства редактирования решили перенести в базу данных.


При всех достоинствах и недостатках, у нас таки получился желаемый «Универсальный конвертер данных на платформе .Net Framework» сейчас он активно работает в модулях публикации товаров на Amazon, Wallmart и другие торговые площадки именно там требуется постоянный меппинг, конвертация и обогащение данных.

© Habrahabr.ru