При использовании архитектуры в стиле вертикальных слайсов, рано или поздно встает вопрос а что делать, если появляется код, который нужно использовать сразу в нескольких хендлерах?
TLDR: нужно создать промежуточный слой обработчиков и добавить специализированные маркерные интерфейсы, чтоб было ясно, какие обработчики холистические абстракции, а какие нет.
Ответ на этот вопрос не всегда очевиден. Джимми Боггард, например предлагает просто использовать приемы рефакторинга. Я всецело поддерживаю такой подход, однако форма ответа видится мне такой-же полезной, как и предложение воспользоваться свободной монадой для внедрения зависимостей в функциональном программировании. Такая рекомендация точна и коротка, но не слишком полезна. Я попробую ответить на этот вопрос более развернуто.
Рефакторинг
Итак, я буду пользоваться двумя приемами рефакторинга:
Допустим, код обработчика выглядит следующим образом:
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<???, ???>
. Этот вопрос можно
разделить на два:
- Стоит ли мне передавать
ICommand/IQuery
в качестве входного параметра? - Стоит ли мне использовать
IQueryable<T>
в качестве возвращаемого значения?
Стоит ли мне передавать ICommand/IQuery
в качестве
входного параметра?
Не стоит, если ваши интерфейсы определены как:
public interface ICommand<TResult>{}public interface IQuery<TResult>{}
В зависимости от типа возвращаемого значения
IDomainHandler
вам может потребоваться добавлять
дополнительные интерфейсы на Command/Query
, что не
улучшает читабельность и увеличивает связность кода.
Стоит ли мне использоватьIQueryable<T>
в
качестве возвращаемого значения?
Не стоит, если у вас нет ORM:) А вот, если он есть Не смотря на
явные проблемы LINQ с LSP я думаю, что ответ на этот вопрос
зависит. Бывают случаи, когда условия получения данных
настолько запутаны и сложны, что одними спецификациями
выразить их не получается. В этом случае передача
IQueryable
во внутренних слоях приложения меньшее из
зол.
Итого
- Выделяем метод
- Выделяем класс
- Используем специализированные интерфейсы
- Внедряем зависимость слоя предметной области в качестве аргументов конструктора
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; }}