Вы подготовились к приходу AutoMapper?
Введение
Данная статья предназначена к прочтению разработчикам и архитекторам распределенных систем на платформе .NET. В ней будет рассмотрен гибкий каркас для объектно-объектного преобразования (далее маппинга). Так же будут рассмотрены некоторые аспекты Domain-Driven Design’а.
Зачем мне нужен объектно-объектный маппинг?
Следуя основным принципам DDD, мы реализуем так называемую Rich Domain Model (эти объекты также должны соответствовать принципу POxO). Объекты реального мира, нашедшие отражение в нашем приложении частенько так же передают достаточную сложность, следовательно, достаточно корректно построенная модель крайне тяжело поддаётся перемещению между слоями приложения (не путать с легкостью вносимых изменений).
Тем не менее достаточно часто появляется необходимость “распределения” (я имею ввиду создание промежуточных сущностей, а не растекание модели по слоям) модели между слоями, для отображения, к примеру, атрибутов её сущностей пользователям (в шаблонах представления MVx), а так же передаче по сервисам (Data Transfer Object). Порой бывает даже, что модель “распределяется” для тестирования некоторых аспектов.
Предположим, мы в Африке, у нас банановая плантация, всё классно, выращиваем, продаём, выращиваем, продаём, но тут внезапно внутренний рынок переполняется и нам надо расширятся (к примеру вести бананы в Россию), мы пишем WCF сервис, который будет слать наши бананы. Так как бананы в Африке имеют несколько иное значение, чем в России, то, соответственно нам понадобятся лишь некоторые атрибуты (остальные фактически не имеют значения), которые мы забубеним в наш DTO
Правильнее было бы дать классу BananaWrapper название BananaDTO, для того, чтобы точно отображать его функциональное назначение, но я оставлю название таким для большего уровня абстракции, к примеру, если нам понадобится сделать автомат по продажи бананов и поместить этот объект в Presenter Model
Хочу заметить, что порой задача преобразования объектов становится довольно-таки нетривиальной и в лучшем случаем выглядит примерно подобным образом (это решение в лоб, есть ещё более изощренные методы
):
- public class Banana
- {
- public string Country { get; set; }
- public double Price { get; set; }
- public int Age { get; set; }
- }
- public class BananaWrapper
- {
- public string Country { get; set; }
- public double Price { get; set; }
- public int Age { get; set; }
- }
- public class BananaMapper
- {
- public BananaWrapper GetWrapper(Banana banana)
- {
- return new BananaWrapper
- {
- Country = banana.Country,
- Price = banana.Price,
- Age = banana.Age
- };
- }
- }
* This source code was highlighted with Source Code Highlighter.
Думаю, что такой код писать, а тем более сопровождать, мало кому будет в радость, в последнее время я как раз частенько встречался с такого рода задачами, и находился в поиске решения проблемы.
AutoMapper
И тут на сцену выходит наш персонаж – AutoMapper, и сразу же говорит мне: – послушай, ты что такое пишешь? тебе не лень? ты не боишься допустить ошибок? хочешь я тебе помогу?!. Я конечно же соглашаюсь, и получаю в ответ следующее решение моей проблемы:
- public class BananaMapper
- {
- public BananaMapper()
- {
- Mapper.CreateMap<Banana, BananaWrapper>();
- }
- public BananaWrapper GetWrapper(Banana banana)
- {
- return Mapper.Map<Banana, BananaWrapper>(banana); ;
- }
- }
* This source code was highlighted with Source Code Highlighter.
Класс, и это действительно всё, что мне понадобится. Сложность, вышележащего примера снизилась в моих глазах до нуля.
Итак, что же за механизмы лежат внутри AutoMapper?
AutoMapper проверяет есть соответствующие поля в указанных типах, соответствие проводится как по имени свойства, так и по его типу. Даже такие нюансы, как Product.Name и ProductName будут учтены и обработаны автоматически (wow!). Плюс ко всему методы GetXXX() будут ложится на свойства XXX (да, ну и естественно для особо раздражительных все эти прелести можно отключить и переопределить всё в своих собственных таблицах соответствия (далее мапках)).
Кастомная конфигурация выглядит примерно следующим образом:
- Mapper.CreateMap<CalendarEvent, CalendarEventForm>()
- .ForMember(dest => dest.EventDate, opt => opt.MapFrom(src => src.EventDate.Date))
- .ForMember(dest => dest.EventHour, opt => opt.MapFrom(src => src.EventDate.Hour))
- .ForMember(dest => dest.EventMinute, opt => opt.MapFrom(src => src.EventDate.Minute));
* This source code was highlighted with Source Code Highlighter.
Кстати, все ваши кастомные конфигурации легко поддаются проверке с помощью следующего метода:
- Mapper.AssertConfigurationIsValid();
* This source code was highlighted with Source Code Highlighter.
Так же не плохо работает с:
- коллекциями
- вложенными мапами
- конверторами типов (к примеру string в int)
- нестандартной логикой инициализации
- форматированием
История
Проект появился в конце’08-начале’09, около полугода находился в версии 0.31, сейчас же добрался до RC 1.0, думаю, что релиз уже совсем скоро.
Overhead?
Дебаты по поводу того, насколько быстрее будет работать AutoMapper и присвоение свойств в ручную (и прочие мульки) я игнорирую, т.к. готов пойти на любые жертвы производительности, если получу ясный, читабельный код. Ах, да, автор AutoMapper позаботился об этих вопросах и написал бенчмарки, смотреть здесь: http://code.google.com/p/automapperhome/source/browse/#svn/trunk/src/Benchmark
Ресурсы
Скачать проект, а так же ознакомиться с исходными кодами можно здесь: http://code.google.com/p/automapperhome/
Обсуждения каркаса здесь: http://groups.google.com/group/automapper-users
Так же примеры использования есть здесь: http://automapper.codeplex.com/
Кстати проект разрабатывает Jimmy Bogard, который так же пишет BDD фреймворк для .NET под названием NBehave.
Евгений Н. ответил:
Сериализовать класс отражением – дело принципиально нехитрое, в J2SE есть такое, и даже с поддержкой контроля версий. Например состояние объекта изменилось – это уже другая версия объекта, получается, – вот ее тоже можно сериализовать, или же наоборот – указать что какое бы состояние объекта не было – все сериализуется, при каждой попытке сериализации и отсылке объекта – как только одна версия. И даже с отношениями _к другим_ объектам в стандартной Java справляются (сериализуются и те, на сколько я понял, но так это не применял).
И даже, например, вот есть какие-то методы у нас, но мы знаем что реально – это нативные методы, и на другой машине (типа дискриптора окна) – такое-то свойство в классе – сериализовать не нужно. Тут просто указываем соотв. ключевое слово (transient) у такого поля, таким образом мы уведомляем систему, что бы она не сериализовала значение этого свойства.
А в некоторых навороченных JEE-фреймворках, вроде Seam – DTO вообще не нужны, принципиально исключается этот момент создания и пересылка их. Как это сделано?
Сначала нужно заметить, последовательно если говорить, упомянув про J2SE, что есть J2EE – и в нем, в EJB 3.0 – DTO-нужны.
Т.е. Seam – это еще уровнем выше, чем J2EE.
В нем применяется двунаправленная инъекция (про однонаправленную Вы наверное знаете, классический пример – паттерн IoC).
В общем все сделано еще круче,
упрощение, что не нужно создавать DTO – очень облегчающий момент (я работал как с «классическими» EJB 3.0, где приходилось создавать DTO, так и с EJB 3.0 в Seam-framework, где работа с ними еще больше упрощена, хотя, казалось бы, куда уж более).
Спасибо за статью, пишите. Очень хорошо все оформлено, и вообще.
Октябрь 8, 2009 at 09:35. Постоянная ссылка.
Nik Govorov ответил:
C автором ошибочка вышла.
Октябрь 8, 2009 at 19:06. Постоянная ссылка.
butaji ответил:
Да, спасибо, поправил
Октябрь 9, 2009 at 10:28. Постоянная ссылка.