...

понедельник, 3 февраля 2014 г.

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, будет через пару недель )


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий