Русский
Русский
English
Статистика
Реклама

CQRS что делать с кодом, который нужно использовать сразу в нескольких обработчиках?


При использовании архитектуры в стиле вертикальных слайсов, рано или поздно встает вопрос а что делать, если появляется код, который нужно использовать сразу в нескольких хендлерах?


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

Ответ на этот вопрос не всегда очевиден. Джимми Боггард, например предлагает просто использовать приемы рефакторинга. Я всецело поддерживаю такой подход, однако форма ответа видится мне такой-же полезной, как и предложение воспользоваться свободной монадой для внедрения зависимостей в функциональном программировании. Такая рекомендация точна и коротка, но не слишком полезна. Я попробую ответить на этот вопрос более развернуто.


Рефакторинг


Итак, я буду пользоваться двумя приемами рефакторинга:


  1. Извлечь метод
  2. Извлечь класс

Допустим, код обработчика выглядит следующим образом:


public IEnumerable<SomeDto> Handle(SomeQuery q){    // 100 строчка кода,    // которые потребуются в нескольких обработчиках    // 50 строчек кода, которые специфичны именно    // для этого обработчика    return result;}

В реальности, бывает и так, что первые 100 и вторые 50 строчек перемешаны. В этом случае, сначала придется их размотать. Чтобы код не запутывался, заведите привычку жамкать на ctrl+shift+r -> extract method прямо по ходу разработки. Длинные методы это фу.

Итак, извлечем два метода, чтобы получилось что-то вроде:


public IEnumerable<SomeDto> Handle(SomeQuery q){    var shared = GetShared(q);    var result = GetResult(shared);    return result;}

Композиция или наследование?


Что же выбрать дальше: композицию или наследование? Композицию. Дело в том, что по мере разрастания логики код может приобрести следующую форму:


public IEnumerable<SomeDto> Handle(SomeQuery q){    var shared1 = GetShared1(q);    var shared2 = GetShared2(q);    var shared3 = GetShared3(q);    var shared4 = GetShared4(q);    var result = GetResult(shared1,shared2, shared3, shared4);    return result;}

В особо сложных случаях структура зависимостей может оказаться весьма разветвленной и тогда вы рискуете нарваться на проблему множественного наследования.

Так что, гораздо безопаснее воспользоваться внедрением зависимостей и паттерном компоновщик.


public class ConcreteQueryHandler:     IQueryHandler<SomeQuery, IEnumerable<SomeDto>>{    ??? _sharedHandler;    public ConcreteQueryHandler(??? sharedHandler)    {        _sharedHandler = sharedHandler;    }}

Тип промежуточных хендлеров



В слоеной/луковой/чистой/порты-адаптершной архитектурах такая логика обычно находится в слое сервисов предметной области (Domain Services).


У нас вместо слоев будут соответствующие вертикальные разрезы и специализированный интерфейс IDomainHandler<TIn, TOut>, наследуемый от IHandler<TIn, TOut>.


Некоторые предпочитают используют вертикальные слайсы на уровне запроса и сервисы на уровне домена. Хотя подход и жизнеспособен, он подвержен тем же недостаткам, что и слоеная архитектура в первую очередь, риском повышения связности приложения. Поэтому мне больше нравится использовать вертикальную компоновку и на уровне домена.


public class ConcreteQueryHandler2:    IQueryHandler<SomeQuery,  IEnumerable<SomeDto>>{    IDomainHandler<???, ???> _sharedHandler;    public ConcreteQueryHandlerI(IDomainHandler<???, ???> sharedHandler)    {        _sharedHandler = sharedHandler;    }}public class ConcreteQueryHandler2:    IQueryHandler<SomeQuery,  IEnumerable<SomeDto>>{    IDomainHandler<???, ???> _sharedHandler;    public ConcreteQueryHandlerI(IDomainHandler<???, ???> sharedHandler)    {        _sharedHandler = sharedHandler;    }}

Зачем нужны специализированные маркерные интерфейсы?


Возможно, у вас появится соблазн использовать IHandler во всех случаях. Я не рекомендую так поступать, потому что такой подход почти наверняка приведет либо к проблемам с производительностью, либо к проблемам с сильной связностью в долгосрочном сценарии.



Интересно, что несколько лет назад мне попадалась статья, предостерегающая от использования одного интерфейса на все случаи жизни. Тогда я решил ее проигнорировать, потому что я сам умный и мне виднее. Вам решать, следовать моему совету или проверять его на практике.


Тип промежуточных хендлеров


Осталось чуть-чуть: решить, какой тип будет у IDomainHandler<???, ???>. Этот вопрос можно разделить на два:


  1. Стоит ли мне передавать ICommand/IQuery в качестве входного параметра?
  2. Стоит ли мне использоватьIQueryable<T> в качестве возвращаемого значения?

Стоит ли мне передавать ICommand/IQuery в качестве входного параметра?


Не стоит, если ваши интерфейсы определены как:


public interface ICommand<TResult>{}public interface IQuery<TResult>{}

В зависимости от типа возвращаемого значения IDomainHandler вам может потребоваться добавлять дополнительные интерфейсы на Command/Query, что не улучшает читабельность и увеличивает связность кода.


Стоит ли мне использоватьIQueryable<T> в качестве возвращаемого значения?


Не стоит, если у вас нет ORM:) А вот, если он есть Не смотря на явные проблемы LINQ с LSP я думаю, что ответ на этот вопрос зависит. Бывают случаи, когда условия получения данных настолько запутаны и сложны, что одними спецификациями выразить их не получается. В этом случае передача IQueryable во внутренних слоях приложения меньшее из зол.


Итого


  1. Выделяем метод
  2. Выделяем класс
  3. Используем специализированные интерфейсы
  4. Внедряем зависимость слоя предметной области в качестве аргументов конструктора

public class ConcreteQueryHandler:    IQueryHandler<SomeQuery,  IEnumerable<SomeDto>>{    IDomainHandler<        SomeValueObjectAsParam,        IQueryable<SomeDto>>_sharedHandler;    public ConcreteQueryHandler(        IDomainHandler<            SomeValueObjectAsParam,            IQueryable<SomeDto>>)    {        _sharedHandler = sharedHandler;    }    public IEnumerable<SomeDto> Handle(SomeQuery q)    {        var prm = new SomeValueObjectAsParam(q.Param1, q.Param2);        var shared = _sharedHandler.Handle(prm);        var result = shared          .Where(x => x.IsRightForThisUseCase)          .ProjectToType<SomeDto>()          .ToList();        return result;    }}
Источник: habr.com
К списку статей
Опубликовано: 18.03.2021 16:05:16
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Разработка веб-сайтов

Net

Проектирование и рефакторинг

C

Cqrs

Vertical slices

Refactoring

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru