Incoding Rapid Development Framework ( part 2 CQRS )

от автора

image

Пред история

Моя предыдущая статья была знакомством с Incoding Framework, которое начиналось с IML (наша флагманская фича ). IML подтолкнул нас развить проект больше, чем набор утилит ( такого добра полно в любой команде разработчиков ) используемых в проектах компании, но это не значит, что другие компоненты не прорабатываются, а напротив «полируются» с не меньшей детализацией и это я попробую Вам доказать.

Я об этом уже писал

Как и прошлый мой пост, этот тоже будет от части компиляцией из статей ( CQRS vs N-Layer, CQRS расширенный курс ) нашего блога, который поможет сразу познакомиться с нашей реализацией популярного подхода CQRS, а также с добавлением новых деталей и комментариев.

Серебренная пуля ?

Раньше я всегда был сторонником того, что у каждого решения есть свои минусы и плюсы, но CQRS на мой взгляд превосходит N-Layer, а также не имеет «противопоказаний» или «побочных эффектов», что делает его кандидатом на первый патрон в обойму, но обо всем по порядку.

Кто-то не слышал про CQRS?

Для тех, кто уже использует CQRS, первые разделы могут быть не интересны, поэтому прежде чем поставить ярлык «велосипед», предлагаю ознакомиться с разделом killing feature, который может Вас убедить в обратном. Тем же, кто использует N-Layer архитектуру, стоит задуматься о переходе на CQRS и чтобы подкрепить свое предложение я опишу наш путь к CQRS

О, как удобно много абстракций ( мнение из прошлого )

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

  • “Разбухание” исходного кода, проблема чаще всего происходит из-за добавления связующих слоев таких как facade layer, communication layer и т.д.
  • Скрытие деталей за множеством уровней слоев.

примечание: идея N-Layer, так же связана с подменной dll определенного слоя, но эта крайне редко востребованная задача и намного проще решается через IoC.

Основным источником «зла» в N-Layer чаще всего бывает service layer, который предназначен для скрытия деталей работы бизнес-процессов и логики приложения, путем агрегации схожих задач в один класс, что со временем превращает его в GOD object и ведет к проблемам:

  • Поддержки и расширения тесно связанных методов
  • Написанию unit test

Если не в даваться в тонкости, то условно переделка на CQRS будет заключаться в разделении больших объектов на мелкие

public interface IUserService {     void Add(string name);      void Delete(string id);      List<User> Fetch(Criteries criteries);  } 

После декомпозиции получаем две Command ( AddUser,DeleteUser ) и query ( FetchUser ), что увеличивает количество классов, но позволяет избавиться от мало связанных методов в классе.
«Теперь ещё больше классов надо написать !» — это первый вопрос, который задают «тру» N-Layer разработчики, но в качестве контраргумента можно выделить, то что мы получаем атомарность ( никаких зависимостей между объектами ) задач, а это дорогого стоит:

  1. Меньше конфликтов в VCS ( Version Controler System )
  2. Поиск по классам проще, чем по методам
  3. Больше не надо разделять UserService на partial, потому что в одном сложно ориентироваться ))
  4. Issue на bugtracker формируется из Command и Query
  5. Sprint для agile формируется из Command и Query
  6. Тестирование мелких объектов

Наша реализация

Для нетерпеливых, кто хочет сразу «пощупать» код, можно скачать исходники ( там же пример связки Incoding CQRS + MVD ) c GitHub, именно его их мы будем рассматривать в качестве примера.
примечание: чтобы запустить проект, необходимо создать пустую базу данных и указать её ConnectionString в web.config ( ключ main )

важно: структура папок и настройка IoC ( ORM, dispatcher and etc ) в статье не рассматривается, но все это описано тут

Dispatcher

Ключевой элемент, который выполняет Message ( Command или Query ) в рамках одной транзакции ( Unit Of Work ).
Controller — в рамках asp.net mvc ( console, wpf, owin and etc ) системы для использования dispatcher, нужно получить его экземпляр из IoC и далее доступны два метода:

  • Push — выполняет command
  • Query — выполняет query

public ActionResult Test() {     var dispatcher = IoCFactory.Instance.TryResolve<IDispatcher>();     var id = dispatcher.Query(new GetIdQuery());     dispatcher.Push(new DeleteEntityByIdCommand(id));     return something ActionResult; } 

примечание: типы возврата и интеграции с Controller описана тут

Unit Of Work – если посмотреть реализацию любой Command, то видно, что основной код содержится в перегруженном методе Execute, который можно вызывать и без участия Dispatcher, но тогда не будет открыто подключение к базе и транзакции.

new DeactivateEntityCommand().Execute(); // without transaction and connection dispatcher.Push(new DeactivateEntityCommand()); // open transaction and connection 
Message

CommandBase и QueryBase являются дочерним от Message, но поведение у них отличается в типе Isolation Level с которым создается Unit Of Work

  • Command — ReadCommitted
  • Query — ReadUncommitted

примечание: ограничение может показаться жестким, но если по каким-то причинам Вам надо сохранить ( удалить, вставить ) данные в Query, то стоит пересмотреть Ваш сценарий путем разделения на более мелкие задачи ( пример далее )

Message имеет два основных инструмента:
Repository — интерфейс для работы с базой данных, поддерживает все сценарии CRUD

Примеры

Create

Repository.Save(new entity()) 
Read

Repository.GetById<TEntity>(id);   Repository.Query(whereSpecification: spec,                                 orderSpecification:spec,                                 paginatedSpecification:spec) 

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

Update

var entityFromDb  = Repository.GetById<TEntity>(id); entityFromDb.Title  = "New title"; // tracking 

примечание: если provider ORM не поддерживает tracking, то нужно вызывать метод Repository.SaveOrUpdate(entity)

Delete

Repository.Delete<TEntity>(id); 

Event Broker — коммуникации между Command, что позволяет агрегировать повторно встречающиеся «куски» кода и инкапсулировать в события и подписчики.

Задача: аудит некоторых действий
Проблема: код для сохранение Audit будет одинаковый и придется его повторять в каждой Command
Решение в Service Layer: можно выделить базовый класс ServiceWithAuditBase, но это будет трудно поддерживать при росте сложности аудита, да и наследование всегда приводит к усложнению.
Решение с подписчиками
Код Event

public class OnAuditEvent : IEvent {     public string Message { get; set; } } 

примечание: условие, чтобы Event реализовывал IEvent
Код Subscriber

public class AuditSubscriber : IEventSubscriber<OnAuditEvent> {     readonly IRepository repository;      public AuditSubscriber(IRepository repository)     {         this.repository = repository;     }      public void Subscribe(OnAuditEvent @event)     {         this.repository.Save(new Audit { Message = @event.Message });     }      public void Dispose() { }  } 

примечание: Subscriber создается через IoCFactory и следовательно можно вводить инъекции в ctor ( конструктор ) или использовать IoCFactory.Instance.TryResolve()
Код Command

EventBroker.Publish(new OnAuditEvent                         {               Message = "New product {0} by {1}".F(Title, Price)                         }); 
Query

Чтобы создать пользовательский Query, нужно наследовать QueryBase, где указать ожидаемый возврат данных и переопределить метод ExecuteResult

public class GetProductsQuery : QueryBase<List<GetProductsQuery.Response>> {     public class Response     {         public string Title { get; set; }          public string Price { get; set; }     }      public string Title { get; set; }      public decimal? From { get; set; }      public decimal? To { get; set; }      protected override List<Response> ExecuteResult()     {         return this.Repository.Query(whereSpecification: new ProductByTitleWhere(this.Title)                                         .And(new ProductBetweenPriceWhere(this.From, this.To)))                          .Select(product => new Response                                                 {                                                         Title = product.Title,                                                         Price = product.Price.ToString("C")                                                 })                          .ToList();     } } 

Можно выделить то, что в качестве Result используется nested класс, но почему не…
Вернуть сразу объект из базы ( Entity ) — это способ имеет проблему связанную с областью работы сессии подключения к базе данных, рассмотрим на примере.

Пример

Код Query

return Repository.Query<Product>(); 

Код Controller

dispatcher.Query(new GetProductsQuery()) .Select(r=> new { Amount = r.Orders.Sum(r=>r.Price)) 

Ошибка будет в runtime, если не выключить Lazy Load ( актуально только для OLAP объектов ) на уровне маппинга ORM, потому что после завершения Query сессия закрывается, а при обращении к полю Orders идет запрос в базу данных.

ViewModel — это тоже самое, что и nested класс, но с возможностью повторно использовать в других Query, что крайне редкий сценарий.

Command

Первые наши реализации Command для CQRS, были с разделением описания ( AddUserCommand ) от исполнителя ( UserCommandHandler ), из-за чего усложнялся процесс разработки, поэтому в дальнейшем были объединены эти части.
примечание: основная причина в разделение была поддержка DTO ( Data Transfer Object ) модель для SOAP систем, но c появлением asp.net mvc, стало просто не актуально

Чтобы создать пользовательскую Command, нужно наследовать CommandBase и переопределить метод Execute

public class AddProductCommand : CommandBase  {          public string Title { get; set; }          public decimal Price { get; set; }          public override void Execute()         {             var product  = new Product                                 {                                         Title = Title,                                          Price = Price                                 }             Repository.Save(product);            Result = product.Id;         } } 

примечание: бывают сценарии, где Command должен вернуть данные, то можно проставить Result в методе Command

Killing feature

Composite

CQRS помогает «дробить» сложные задачи, на более мелкие, но возникает проблема общей транзакции выполнения.
Задача: сохранение объекта в 3 этапа
Решение: разделяем на три command ( Step1Command, Step2Command, Step3Command )
Условие: транзакционность

public ActionResult Save(Step1Command step1,Step2Command step2,Step3Command step3) {     dispatcher.Push(composite =>                         {                             composite.Quote(step1);                             composite.Quote(step2);                             composite.Quote(step3);                         });     return IncodingResult.Success(); } 

Кроме группировки Command в один пакет, Composite позволяет манипулировать результатами выполнения. Усложним задачу и поставим условие, чтобы Step1 после выполнения передавал Id нового элемента в Step 2 и 3.

public ActionResult Save(Step1Command step1,Step2Command step2,Step3Command step3) {     dispatcher.Push(composite => {     composite.Quote(step1,new MessageExecuteSetting {               OnAfter = () => { step2.Id = step1.Result;  step3.Id = step1.Result; }                                                     });     composite.Quote(step2);     composite.Quote(step3);}); } 
“Горячая” смена connection string

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

dispatcher.Query(query, new MessageExecuteSetting                  {                     Connection = new SqlConnection(currentConnectionString)                  }); 

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

Обертка поверх “старой″ базы не проблема в Incoding Framework. Имеются средства, которые позволяют избежать создания дополнительной инфраструктуры для работы с разными конфигурациями и разрабатывать Command и Query не учитывая этой детали.

dispatcher.Query(query, new MessageExecuteSetting                             {                                     DataBaseInstance = "Instance 2"                             }); 

примечание: к каждому ключу принадлежит своя конфигурация ORM

Заключение

Статья делает упор в первую очередь на обзор реализации Incoding CQRS, поэтому обзор непосредственно самой методологии CQRS краткий, да он и так хорошо описан в других источниках. Incoding CQRS — это одна из частей нашего framework, но она полностью самодостаточная и применяется без других компонентов ( IML, MVD, Unit Test ).

В комментариях к первой статье о IML, были вопросы о возможности использования на альтернативных ( OWIN ) платформах для asp.net mvc, поэтому сразу замечу, что Incoding CQRS применялся в WPF проектах, а что касается IML, то в этом месяце будет статья о интеграции.

P.S. Рад услышать отзывы и комментарии, а также вопросы по работе framework. Если будет интерес к статьям о framework, то следующий зеленый герой Incoding MVD, будет через пару недель )

ссылка на оригинал статьи http://habrahabr.ru/post/211206/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *