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

ссылка на оригинал статьи https://habr.com/ru/post/547746/


Комментарии

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

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