Pull to refresh

Incoding Rapid Development Framework ( part 2 CQRS )

Reading time 7 min
Views 6.5K
image

Пред история


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

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


Раньше я всегда был сторонником того, что у каждого решения есть свои минусы и плюсы, но 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 и ведет к проблемам:
  • Поддержки и расширения тесно связанных методов
  • Сложно покрывать тестами большой объект


Если не в даваться в тонкости, то условно переделка на 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 )

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;
}


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 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. Следующий зеленый герой Incoding MVD, которые сражается против copy&paste будет через пару недель )))
Tags:
Hubs:
+1
Comments 7
Comments Comments 7

Articles