Пред история
Моя предыдущая статья была знакомством с 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 разработчики, но в качестве контраргумента можно выделить, то что мы получаем атомарность ( никаких зависимостей между объектами ) задач, а это дорогого стоит:
- Меньше конфликтов в VCS ( Version Controler System )
- Поиск по классам проще, чем по методам
- Больше не надо разделять UserService на partial, потому что в одном сложно ориентироваться ))
- Issue на bugtracker формируется из Command и Query
- Sprint для agile формируется из Command и Query
- Тестирование мелких объектов
Наша реализация
Для нетерпеливых, кто хочет сразу «пощупать» код, можно скачать исходники ( там же пример связки 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 ) — это способ имеет проблему связанную с областью работы сессии подключения к базе данных, рассмотрим на примере.
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.
Комментариев нет:
Отправить комментарий