MediatR является, безусловно, очень успешным и популярным open-source проектом, который глубоко проник в недры коммерческой разработки. Однако, сколь популярен инструмент столь много способов его применения рождается среди сообщества. В этой статье я покажу два самых главных способа использовать этот инструмент неправильно вместе со способами решения проблемы.
1. Вызов одного обработчика из другого обработчика
Рассмотрим следующую ситуацию. Допустим, вам надо обработать некоторое сообщение:
public class PingHandler : IRequestHandler<Ping, string>
{
public Task<string> Handle(Ping request, CancellationToken token = default) =>
Task.FromResult("Pong");
}
Затем вдруг пришли новые требования к логике (чтоб их...), и после обработки этого сообщения вам нужно запустить обработку других сообщений на основе результата работы исходного обработчика либо входящего сообщения. Как делает большинство в данной ситуации:
public class PingHandler : IRequestHandler<Ping, string>
{
private readonly IMediator _mediator;
public PingHandler(IMediator mediator) => _mediator = mediator;
public async Task<string> Handle(Ping request, CancellationToken token = default)
{
await _mediator.Send(new AfterPing(request), token);
return "Pong";
}
}
Так делать плохо, забудьте такой bad practice.
В первую очередь это выглядит крайне запутанно. У MediatR итак проблемы с навигацией через IntelliSense, не надо их усугублять.
Также, стоит вспомнить, что by design этот инструмент служит внешним мостом к фактическому поведению вашего приложения, которое ещё и специфично для выбранной предметной области. Про это как раз обложка статьи.
Получается, что вызов обработки других сообщений изнутри обработчиков это антипаттерн применительно к самой библиотеке. Со слов разработчика библиотеки:
The indirection of a handler is good at the application level, but just got confusing once we got inside a handler (and it introduced coupling).
Ведь цель инструмента сделать точки входа на верхнем уровне приложения по модели "запрос-ответ", максимально минимизируя связанность. Ровно противоположно тому, что манифестирует код выше.
Есть много способов избежать этой проблемы и упаковать эту логику:
Возвращать данные для формирования сообщения и вызывать его "наверху", там где был вызов исходного обработчика (самый элементарный способ);
Отдельный класс;
Доменный сервис;
Метод расширения;
Специальный сервис;
Пайплайны MediatR;
и так далее.
2. Обработка множества сообщений в одном handler
Допустим у вас есть некоторый скоп сообщений для работы с одной и той же сущностью. Поскольку там плюс минус одни и те же зависимости вы решили не писать несколько обработчиков, а слить их в один, и получили что-то такое:
public class MyEntityRequestHandler :
IRequestHandler<CreateMyEntityRequest, MyEntity>,
IRequestHandler<GetMyEntityRequest, MyEntity>,
IRequestHandler<GetAllMyEntitiesRequest, List<MyEntity>>
{
...
}
Опять же мы идём против идеи библиотеки: разделить приложение на набор неповторяющихся запросов для повышения его гибкости и лёгкости сопровождения. Такой подход это возврат к методике разработки с раздуванием интерфейсов, нарушающей ISP.
Ну и, со слов разработчика библиотеки:
Don't combine your handlers, keep them separate, reduce coupling across handlers.
Вывод
В рамках статьи были рассмотрены две главные ошибки с объяснением, почему это ошибки и как их избежать. Почему они главные? Дело в их распространённости и сути: они идут противоречат design principles, отражённым в документации.
Также об этой теме можно подробнее почитать в источниках:
Ещё я веду telegram канал StepOne, где оставляю много интересных заметок про коммерческую разработку и мир IT глазами эксперта.
Комментарии (3)
Ordos
25.08.2022 13:56Я категорически не соглашусь с первым пунктом.
Для меня медиатор - это в первую очередь инструмент организации кода. Да, у него есть проблемы с IntelliSense, да есть некоторый бойлерплейт, но с другой стороны решается проблема раздутых сервисов с кучей зависимостей. Тут уж кому как больше нравится.
И в этом месте в случае больших проектов с кучей логики я не вижу вообще никакой проблемы вызывать одни обработчики из других, главное, чтобы они в проекте лежали рядом с реквестом, чтобы их просто было найти.
А то, что, приводится в примере вообще больше похоже на нотификацию, которые в медиаторе тоже обрабатываются через обработчики и обычно вызываются как раз таки из других обработчиков.
yar3333
Статья оборвалась для меня очень неожиданно :(
Stefanio Автор
Согласен, есть такое! Однако, для тех кому не хватило оставил источники в конце и основной посыл статьи достаточно простой.
Когда используете какой-то инструмент, всегда держите в голове идею и point его создателя, чтобы не противоречить базовым design principles. Потому что, если выходит так, что не получается использовать инструмент по назначению, проще не тратить силы на попытки его встроить, а сменить или отказаться