Как стать автором
Обновить

Комментарии 29

Нет никакого медиатора между ядром и инфраструктурой. Нет медиатора внутри ядра. Медиатору отведено единственное четкое место, где он располагается, это интерфейс ядра приложения

Почему именно так? У вас на проекте 1300 команд/квери, столько же хэндлеров, и у них нет общего кода? Этот общий код можно выносить в сервисы из кучи методов и циклических зависимостей, и мы просто всю эту лапшу выносим чуть глубже в ядро. Зачем, если есть медиатор? Максим Аршинов так же упоминал в своем докладе, что будут доменные юз кейсы, но, главное, не делать их командами и квери, а чем-то иным. Мне это кажется отличным подходом. Или вы из-за перфоманса якобы против?

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

При этом у нас существуют еще инфраструктурные сервисы, доменный слой, инфраструктурные поведения, конфигурации, которые тоже позволяют избегать дублирования. Экстеншены, в конце концов. Варианты для удовлетворения потребности в DRY присутствует.

Почему бы не вынести в квери и команды вообще все? У меня были такие проекты и лично мне показалось, что именно при таком использовании медиатор создал больше проблем, чем принес пользы. Откуда-то оттуда наверняка и растут ноги критики.

  1. Сложно стало договариваться с коллегами, что в принципе может быть командой или кверей, а что нет.
    Например, кто-то может сделать пулреквест с крутой командой сохранения агрегата. Назвать ее CreateSomethingGood. И вроде пока мы знаем, что это просто сохранение в базу, но через полгода легко можно и самому вызвать этот самый CreateSomethingGood из контроллера, забыв что мы пропустили таким образом кучу логики (например по отправке событий). Без четкого места медиатора становится сложно аргументировать коллегам, да и себе, где же эти команды можно создавать, а где нельзя. Очень сильно размываются границы между слоями. Можно попробовать бороться с этим неймингом, но.. не хочется.

  2. Аспекты, которые в случае с одним местом медиатора, почти всегда исполняются один раз в понятном месте, могут начать исполняться в непредвиденном месте. Сложность написания поведений увеличивается в разы. Если с подходом описанным в статье, нам надо только помнить об одном вызове поведения, на входе в слой приложения, то с кучей команд в разных местах это предположение будет неверным.

Это больше интуитивное решение, не отрицаю что можно построить классную архитектуру исключительно на медиаторе, но я столкнувшись с проблемами, решил их таким вот ограничением. Мне показалось это хорошим компромиссом.

и самому вызвать этот самый CreateSomethingGood из контроллера

объяснение более чем разумное, вот только можно легко сделать чтобы проект с контроллерами не имел ссылку на проект DAL!?

А что вы используете в качестве "медиатора"? MediatR? Ведь опять же, если посмотреть на шаблон, то медиатор определяет контракт. Значит для разных задач -- разный контракт. Вместо одного god-IMediator, может надо бы сделать ICommandBus, IDataQueryBus, ISomeotherBus? И они не могут принимать какие угодно команды/квери, а строго определённые (наследующиеся от базовых команд/кверей конкретного типа). В таком случае в рантайме очень легко бить по рукам за попытку вызвать команду из команды, это легко и будет падать с исключением. Всяко лучше соглашений, за которыми нужно постоянно следить и бдить.

Таки зачем мне нужен медиатор то?

Всегда смотрел на него как на сервис локатор, потому что им и является.

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

Таки мне нравится как Mark Seemann описывает теорию Dependency Injection в книге. Оно складывается в рабочую красивую картину, а всё что не складывается - надо с подозрением осмотреть, почему хочется так сделать. На больших проектах допускаю какие то точечные исключения из общей схемы, но в целом схема работает и дает удобство работы с зависимостями.

А можете объяснить для тех, кто сложнее, чем дай-положи апи ничего не делал? Вот у нас есть контроллер, в него приходит запрос. Вероятно где-то чуть раньше запрос уже залогирован через миддлварь, где-то на уровне контроллера мы его провалидировали. Теперь из контроллера он уходит в сервис, который пропускает это всё через бизнес логику и просит данные у базы, вероятно через другой сервис. Какую проблему можно решить в этой схеме добавлением Медиатра? А если никакую, потому что, вроде, всё предельно просто, то что нужно усложнить, чтобы Медиатр оказался полезен? Я с удовольствием приму в качестве ответа ссылку на уже существующую статью или доклад, где тема будет раскрыта для простых парней вроде меня, а не для знатоков многослойных архитектур, чьи проблемы мне, увы, не понять.

Большинство архитектурных инструментов, в том числе и медиатор, сделаны чтобы бороться со сложностью. Если сложности нет и она не планируется, то инструменты использовать не нужно, даже скорее вредно.

В вашем примере, представьте что помимо HTTP вызовов вы начнете слушать очередь в rabbitmq, и из обработчика сообщений дергать тот же самый сервис. Теперь логика, которую вы писали и тестировали на middleware не будет работать, эти вызовы будут для нее невидимыми. А может быть это будет не очередь, а простенький BackgroundService, который что-то там чистит раз в час. За него aspnet тоже ничего не залогирует.

Или может быть другая ситуация, что у вас так и остаются простенькие сервисы, но вот один работает с aspnet-ом, а другой слушает сообщения из очереди. Поведение Медиатора, которое бы вы написали 1 раз в общей либе, работало бы одинаково для этих случаев.

Плюс поведения работают все таки с конкретными командами или кверями, это больше уже абстракция бизнес действий, с явными аргументами. А middleware в aspnet с запросом пользователя, другой уровень абстракции, другие возможности.

Ага. Я понял идею с, так сказать, сдвигом точки входа в приложение с момента получения запроса на момент вызова сервиса. Это на ваши диаграммах понятно показано. Но почему бы тогда мне просто не перенести валидацию на уровень сервисов? Зачем нужны команды и хендлеры? Извините, если покажется, что я троллю - я просто не понимаю.

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

Это в спринге достаточно контроллер обозначить и его все метрики придёт скрапер к8с и соберёт в прометиус и т.д. Логи, MDC, трейсы ровно как и транзакции - все через аннотации можно использовать. Доступ к данным сейчас вообще без нашего кода работает, а нет кода - нет тестов. Ну а тут чистая битва с мельницами и не надо ей мешать, нам платить будут больше.

За 15 лет вообще не использовал ни разу гоф. Ну как, я их вижу то там, то сям, но это уже готовые механизмы фреймворков. А в серверлесс они вообще исчезают, т.к. там упор на функциональное программирование со своими тараканами.

В .NET уже давным-давно так. И логи, и метрики, и трассировки, которые пробрасываются, отправляются и принимаются, из коробки без вашего участия. И всё даже без аннотаций. Всё даже ещё сильно круче, MDC устарело давно как класс, и всё просто кладём в багаж, и оттуда разлетаются куда угодно, хоть в Zipkin, хоть в Jagger, хоть в логи, хоть в метрики.

По отзывам наших девопсов, проекты на .NET -- отдушина. Оно просто работает. Ещё из коробки хелсчеки какие хочешь, активные, пассивные, реди/лив, легко прикрутить отчёты по хелсам.

И ещё счётчики производительности до винтиков и байтиков, все собираются хоть скраппером, хоть сбросом в коллектор. Изумительно. Поэтому я бы не был так категоричен :)

О, рад слышать, что до мира мс докатилась цивилизация. Уход "отцов основателей" был им на пользу и бигтех вот-вот перейдёт на их прекрасные решения. А нет, всем пофиг, т.к. вендорлок.

Майки не меценаты, у них конкретный шкурный интерес, да развивают .NET семимильными шагами, понятно ради выгоды, а выгоду они видят в своих облаках. Скрепя зубами вылезли в опен сорс, но вроде не прогадали. Однако расслабляться рано, вендор это всегда риск.

Знаю, что совсем не в тему, но кто-то же должен это озвучить )

Дочь как раз вчера подошла и сказала "Папа мне нужен медиатор"

Ну я и отдал ей закрытую на днях банковскую карту и ножницы. У нее есть 3 медиатора.

Какая большая статья про паттерн «Команда» :)

Прочитав название думал, что статья про то как перестать играть на гитаре пальцами....

Если вдруг вы решили в UI слое не делать никаких DTO-х, а принимать команды и квери прямо в контроллеры

Меня часто посещает это идея, но я все же не могу на это решиться и принимать команды и квери в контроллере.

Можете пожалуйста рассказать про свой опыт, если он был? С какими трудностями столкнулись с таким подходом и какие преимущества это вам дало?

Я думаю, что этот подход может работать, но в каких-то ограниченных случаях. Например, если вы фулстек команда и фронт разрабатываете сами рядом с бекендом. Потому что вместо какого-то независимого контракта, у вас получается контракт который строго продиктован бекендом. Вы уже не сможете по просьбе фронтов что-то там подправить, поменять название поля, потому что править команды и квери из-за запросов UI слоя это максимально неправильно. Еще подход хорошо будет работать, если вы пишете контроллеры не для сторонних клиентов и ходить в них будете исключительно из кода других сервисов, а может и вообще сгенерите себе клиента.

Из плюсов код становится проще. Не нужно ничего мапить, перегонять туда сюда модельки, в принципе эти дтохи создавать и синхронизировать с изменениями в кверях\командах. Приняли объект в контроллер, его же и кинули в медиатор.

Минусы тоже есть:

  • Вы не сможете контекстные данные из контролера добавить в команду. Например, id пользователя, который лежит в токене, или его роль, или какое-нибудь значение из header-а. Точнее сможете, но костылями, какими-нибудь with на рекорде.

  • Вложенные модельки будут по умолчанию криво мапиться в квери, с точкой в названии, тоже мало кому из фронтов понравится. С командами все хорошо, потому они просто json в теле.

  • Не получится делать красивые REST урлы, потому что в REST айди ресурсов прописываются в пути до ресурса, а у вас принимается моделька квери. Или делать урлы некрасивые, либо костылями перед передачей в медиатор подсовывать параметры из пути в команду через with.

Но кто-то и так не заморачивается с REST, ему не нужны контекстные данные, и все равно на названия квери параметров. Тут зависит от задачи, и от ваших предпочтений.

Мы так иногда делаем. Более того- я считаю такой подход правильным. Из минусов - если вы любите валидацию на атрибутах, их нужно будет в команды тащить, что не то что бы плохо, но в целом норм. Хотя становится проще переиспользовать сервис, есщи вдруг захочется, т.к. он не надеется на валидацию, а проверяеь все сам.

Прокомментирую минусы из коммента выше:

  1. Контекстные данные из контроллера можно передавать отдельной абстракцией. Типа если мне нужна инфа о пользователе - в хэндлере ставите зависимость от (условно) IUserProvider, и делаете на Web слое сервис, которые реализует IUserProvider и получает данные из токена. Если нужно что то из header'ов и т.к. - аналогично. А лезть в контроллере в хидеры или в токен - мне кажется такое решение выглядит не очень. Реальная проблема (ну можно так сказать), если принимаем в контроллере что то в теле, а что то - в query\url, тогда несколькими параметрами передавать придётся в сервис, что в целом не плохо, но уже не совсем команда :)

  2. Не понятно, в чём проблема с вложенными модельками. Можно их не делать вложенными и никаких точек в названии.

  3. Опять же непонятно в чём проблемы, принимает моделька query и что? При чём тут путь до ресурса, что с ним не так? Ни разу с проблемами тут не сталкивались.

Из плюсов - нет лишних перекладываний данных туда сюда. Контроллер становится максимально тупым.

Сколько читаю статей про "медиатор", так и не понял, а где тут медиатор? :) Вроде как шаблон предполагает решение для взаимодействия между объектами-коллегами (или классами-коллегами), а вместо этого обсуждается "команда" для передачи сообщений между разными слоями абстракций, которые коллегами не являются. Или просто решили переопределить и переиначить существующие паттерны?

А ещё забавно, что бед пректикс считается вызов из обработчика одной команды другой команды. Для чего как раз и был создан медиатор -- для общения между коллегами, т.е. как раз посредник.

И далее. А можно ли тогда вообще всё делать через предложенный механизм команда-посредник? Вообще всё-всё через MediatR писать. Любой сервис. Заманчиво. Отказаться от устаревших интерфейсов совсем. Если нет, почему? Где границы? Да, в статье указано, что место "медиатора" в ядре, но почему? Если он так хорош, почему бы не строить всё на нём? Например, те же контроллеры, в принципе можно убрать, определить команду-запрос, который как-нибудь выполнится. Замечательно, мы не знаем как, но знаем что точно выполнится.

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

Ну вот я постарался в самом начале статьи и написать, где тут медиатор. С изначальным шаблоном тут связь скорее историческая, чем реальная.

На вопрос, почему не стоит все строить на медиаторе отвечал в комментариях выше, да вы и сами ответили в своих рассуждениях. Границы выбираются эмпирическим путем. В разных проектах они могут быть разные, возможно, эти границы даже можно сдвигать какими-нибудь хитрыми решениями!

Вы постарались, но всё же вам это не удалось. Единственная причина, почему MediatR вдруг начали называть медиатором, это неудачное название библиотеки. Ситуация как с Xerox -- обыватели часто именуют любой копировальный аппарат "ксероксом". Также это напоминает обыкновенную привычку далёких от IT людей, называть монитор "компуктером".

И это очень плохо, так как медиатор это не просто какой-то там "исторический" шаблон. Это конкретная идея, которая заключается в том, что разработчик отселяет логику взаимодействия между объектами одного ранга (коллегами) в отдельный компонент -- посредник. Т.е. он явно там пишет логику координации взаимодействия.

Это хороший шаблон, но как снежный ком её подменяют совершенно другой идеей, один ляпнул и понеслось.

MediatR является диспетчером команд. Это не посредник, так как никакую явно программируемую логику координации не содержит. Диспетчер не обеспечивает взаимодействие между объектами от слова совсем. Он по принципу Service Locator позволяет вызвать обработчика любой команды откуда угодно. Т.е. в одну сторону, и не подразумевается, что объект-инициатор каким-то образом будет участвовать кроме получения результата.

В вашей статье вы говорите, что MediatR является инструментом борьбы со сложностью, но на деле, эта сложность никуда не уходит. Он её просто маскирует, создаёт видимость, что всё хорошо.

Давайте посмотрим:

public class SomeController
{
  public SomeController(
    IAuthService authService,
    IThumbnailsService thumbnailService,
    IAccauntManager accountManager,
    IUserManager userManager,
    IQrCodeGenerator qrCodeGenerator,
    IFileProvider fileProvider,
    IContractStorage contractStorage,
    IEventDispatcher eventDispatcher,
    IAccountManager accountManager,
    IStoreDispatcher storeDispatcher,
   ...
}

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

А теперь, волшебство!

public class SomeController
{
   public SomeController(IMediator mediator)
...
}

Красота? Красота! Теперь никаких проблем мы не видим. Мы всю грязь просто замели под ковёр.

Ещё хотел сказать, в статье вы ссылаетесь на чистую архитектуру, с самого начала. И тут же её нарушаете. При чём не просто нарушаете, а нарушаете довольно в грубой форме.

Только, прошу вас, коллеги-любители медиатора, давайте не заводить по папке на каждую кверю, это и правда выглядит так себе ? Либо кверя и под ней хендлер, либо отдельный файл в вертикальном слайсе под все квери и команды. Сейчас с существованием рекордов нет никакого смысла создавать отдельные файлы для 3-5 строчных типов.

Лично я использую либо подход с одним файлом, либо, выделяя название квери, нажимаю CTRL+T. Поиск решарпера и райдера всегда ставят хендлер на второе место, он умный.

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

На самом деле замечаний много. Сам паттерн диспетчер команд хороший, то правда, без иронии, и его действительно можно применять, получая большие выгоды. Но именно с MediatR никаких выгод достигнуть нельзя, его использование противоречит многим принципам, то, как он чаще всего используется -- противоречит чистой архитектуре, ну ещё до кучи как будто реализует AOP через behaviour, но на самом деле создаёт ситуацию "скрытой" (или магической) логики. Это грязный Service Locator -- выполнить любой обработчик любой команды из любого места. И люди от него в восторге зачастую ровно по той же причине, как когда-то люди были в восторге от Service Locator. Да и было время, когда goto считался отличным инструментом.

Я очень надеюсь, что скоро программисты научатся видеть очень серьёзную разницу между медиатором и диспетчером команд. Откажутся от MediatR и научатся писать и использовать правильные диспетчеры команд, которые не имеют огромной кучи проблем MediatR-а, а также научатся применять его правильно, с правильным направлением зависимости в концепциях чистой архитектуры. Ну или по крайне мере не будут говорить, что используют чистую архитектуру, там где этим даже не пахнет :)

И каким образом можно решить эту проблему с контроллером? Как ни крути ничего хорошего не выйдет. Худо-бедно разве что с помощью внедрения в экшн-метод используя FromServices.

А это и не проблема. Как раз такой контроллер - сразу видно, что завязан на кучу всего.

Считаете что это плохо - разделяете контроллер на разные ответственности (на два-три), либо группируете зависимости и их логику в рамках контроллера в новые классы ( IAuthService authService, IAccauntManager accountManager, IUserManager userManager) - вот эти трое например всегда используются вместе и вы их выносите в какой-нибудь новый ISomeUserManager.

Понятно, что есть Facade, что можно разрезать на пару контроллеров, но смысл? Контроллер группирует запросы (а мы их наоброт раскидаем, тестировать проще, SRP, но теперь больше однотипного кода, пачка файлов и дублированные префиксы роутов и верятно еще что-то), а вынос в Facade ничем сильно не помогает, все равно будут создаваться ненужные для конкретного запроса сервисы, да и переиспользовать его маловероятно.

Спасибо за статью! Очень четко написано и про чистую архитектуру (напрмиер, что триггерами юскейсов являюстся API, мессаджи и бэкграунд джобы), про плюшки и сложности медиатора тоже хорошо. Поулчил удовольствие от чтения, такие статьи - редкость!

И есть пара комментариев:
1. На картинке написано "CQRS via Mediatr" (хотя больше нигде в тексте про CQRS нет упоминаний). Подход Vertical Slice Architecture, где Application логика реализована в виде отдельного хендлера на каждый юскейс (и Медиатор как инструмент, облегчающий реализацию такого подхода), никак не связаны с CQRS. CQRS - это про разделение стеков чтения и записи на основе отдельных моделей для чтения и записи. Можно реализовать CQRS сделав ReadApplicationService и WriteApplicationService, не обязательно при этом вытаскивать каждый юскейс в отдельный хендлер. Также и вертикал слайсы с помощью медиатора могут быть реализованы в без CQRS с его отдельными моделями и стеками для чтения и записи. Так что вертикал слайсы и CQRS это независимые вещи.

2. Про плюшки - нпонятно какие достоинства от сериализуемости команд. Мне например никогда не приходилось их логгировать. Логгировать удобно HTTP реквесты или мессаджи, т.е. контракт, а зачем логгировать команды? Они передаются от контроллеров к хендлерам, они не должны сериализоваться

3. Отдельный тип для команды так же позволяет нам легко помечать ее с помощью атрибута (это пригодится нам позже).
Не увидел где пригодилось :) Наверное речь об АОП (по списку атрибутов хендлера добавлять ему бехавиоры)? Но удобнее делать это через маркерные интерфейсы, сейчас даже MS DI контейнер поддерживает gneric constraints.

Спасибо за отзыв!

  1. Да, CQRS скорее использован в обывательском смысле, чем в правильном. Речь шла про разделение логики на команды и квери, нужно подобрать какой-нибудь более подходящий термин. CQS?

  1. Я довольно часто использую при отладке. Включаю логирование определенных команд через конфиг, рестартую контейнеры и смотрю в логи. HTTP реквесты логировать бывает не очень удобно. Часть данных может быть в теле и не видна, часть лежать в хедерах. Команды, которые могут быть инициированы и из message bus-а, и из контроллеров, тоже выглядят в логах одинаково и аккуратно.

  2. С интерфейсами интересно, но только для включения/отключения. В атрибуты же можно добавлять свойства, и настраивать таким образом поведение bеhavior. Например, указать уровень изоляции транзакции для конкретной команды. Можно конечно наделать интерфейсов под каждый уровень, но... такое.

  1. Мне нравится термин Vertical Slice Architecture, который придумал Джимми Богард, автор медиатора. Это наводит порядок в терминах: вертикал слайсы - это реализация application логики в виде отдельного хендлера для каждого юскейса (при этом не важно команда это или запрос), а cqrs - это разделение стеков чтения и записи (при этом не важно сделан application уровень сервисами или хендлерами).

    ВонВернон в RedBook называет выделением команды в отдельные хендлеры Специализированным подходом. Но при этом непонятно что делать с запросами. И больше я этого термина нигде не встречал )

    Так что мне больше всего нравится термин вертикал слайсы

Второй вариант, это располагать обработчик прямо под кверей или командой в том же файле.

В студии ещё можно "кастомный нэстинг" настроить.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории