Начинаем моделирование предметной области по заданию для проекта аукциона.
По мотивам нашего большого стрима рассмотрим домен и поддомены для нашего бизнеса с точки зрения DDD. Перейдём на Event Driven архитектуру для проведения сложных бизнес-процессов в системе слабосвязанных модулей. Поговорим про сложности моделирования и познакомимся с практикой Event Storming для построения цепочек команд и доменных событий:
- 00:00:55 Подобласти или поддомены
- 00:02:44 Единый язык всех сотрудников
- 00:03:58 Ограниченные контексты языка
- 00:04:59 Важность коммуникаций
- 00:06:38 Единый формат диаграмм
- 00:08:41 Модули проекта
- 00:09:38 Модульный монолит и микросервисы
- 00:12:52 Устройство модуля аутентификации
- 00:18:00 Минимизация связей
- 00:19:45 Выполнение сложных процессов
- 00:22:34 Транзакция в монолите
- 00:25:59 Множественные вызовы из контроллера
- 00:28:11 Неудобные связи
- 00:29:01 Уведомление о событиях
- 00:31:11 Слушатели событий
- 00:32:22 Цепочки команд и событий
- 00:36:21 Event Driven архитектура
- 00:39:24 Надёжность получения и и повторы
- 00:42:37 Удобство диаграммы событий
- 00:45:42 Кто должен рисовать
- 00:47:18 Сложности процесса проектирования
- 00:48:48 Практика Event Storming
И в следующих эпизодах займёмся практикой моделирования нашего проекта.
Скрытый контент (код, слайды, ...) для подписчиков.
Открыть →Чтобы не пропускать новые эпизоды подпишитесь на наш канал @deworkerpro в Telegram
Спасибо!
Огонь.
Дмитрий, подскажите пожалуйста.
Если у нас есть домен отвечающий за город пользователя. Есть команда
Command\SetCurrentCity
Далее остальные модули использую текущий город пользователя.
Получение текущего города пользователя будет
GetCurrentCity
, будет Command или Query ?Модуль «город пользователя», не имеет никакого контекста, следовательно не может быть модулем. Объясню. Город фактического место нахождения человека может быть один, город для поиска работы может быть другой, город для размещения объявлений третий, город для заказов четвёртый, город для расчёта налогов пятый. Суть, думаю, уловили.
Так же помимо самого города пользователя может требоваться и другие бизнес правила связанные с городом. Например, для договора достаточно указать название города, а вот для расчёта доставки пиццы требуется не только город, но и много чего ещё. Так же и для оценки стоимости авто в разных городах могут отличаться. В общем, в зависимости от контекста город может иметь разные требования. Так же он может где-то быть Value Object, а где-то Entity.
Это было отступление. Теперь о вашем вопросе.
Command - это мутация, команда. Она никогда не возвращает результат. Хотя есть компромисс в виде ID.
Query - это запрос. Она всегда что-то запрашивает, но ничего не мутирует (не изменяет) и получает результат запроса.
Соотвественно GetCurrentCity всегда Query.
"Она никогда не возвращает результат. Хотя есть компромисс в виде ID."
Подскажите, а почему команда не может возвращать результат?
Причина заключается в том, что команда может выполняться очень долго и такую команду делают асинхронной (выполняется в фоне воркерами). То есть команда может отправиться в очередь и полностью выполнится только через несколько минут или часов. Соответственно результат выполнения можно узнать только запрашивая статус из очереди или при получение результата виде события. То есть команда может выдать результат только через событие, которое сгенерируется после её выполнения.
Почему допустимо возвращать ID? Потому что такую команду будет проще сделать асинхронной передавая ID в саму команду и убрав возвращаемый ID из Handler. Проще рефакторить. Но лично я никогда не возвращаю никакой результат из команды (void), а ID сразу передаю в контроллере, сгенерировав его специальным сервисом UuidIdentifierGenerator, который подключается через DI везде где нужно создать ID.
ID в командах обычно возвращается тогда, когда ID в системе автоинкрементный. Если в системе используется UUID, то его нет никакого смысла возвращать из команды. Его можно добавить в саму DTO команды, генерировать снаружи, в контроллере или другом месте вызова.
Вот полезная статья: https://habr.com/ru/post/347908/
Я понимаю случай с асинхронной обработкой. Но если все делается синхронно(и мы точно никогда не будем это менять), не вижу препятствий вернуть необходимые данные после того как данные каким-то образом были изменены.
Я имею ввиду, что все зависит от требований/задачи, которые необходимо решить и фразу "Она никогда не возвращает результат." я бы перефразировал: "Она может не возвращать результат."
Я говорю вам о том, что принято. Вы можете отступать от правил как угодно. Можете хоть в контроллерах напрямую SQL запросами изменять данные в базе. Дело ваше.
Что касается того, что никогда не будет асинхронности - никогда не говори никогда. Если проект живой и растет, то это время приходит всегда.
Если вам нужен результат - после команды можете вызвать Query и отдать результат на фронт.
Начал читать статью и понял, что я ее читал когда-то и полностью согласен со следующим:
"Обработчик может возвращать результат выполнения операции. Он не должен заниматься работой Query — поиском данных, что не значит, что он не может возвращать значение. Главным ограничением на этот счет являются ваши требования к системе и необходимость использования асинхронной модели взаимодействия."
А результатом выполнения операции может быть что угодно, например, сгенерированный ник пользователя после регистрации.
Я же говорю, можете возвращать, если не используете шину. Но если делаете всё хорошо и через шину, то лучше этого не делать. Если нужен результат - можете сделать запрос по Query в контроллере после выполнения команды.
А вот если вам захотелось получить данные из QueryHandler внутри обработчика команды, скорее всего это значит, что CQRS в данной подсистеме не нужен. CQRS решает конкретную проблему - проблему с нагрузками.
Спасибо!
Вопрос не по теме, но я написал неделю или больше вам на почту и не получил ответ, продублирую сюда.
Планируется ли сделать курс по ларавел новой версии? (возможно обновить старый курс который вы когда то делали или сделать полностью новый).
Мне нравится ваш стиль уроков, было бы интересно по ларавел тоже послушать.
А в чём разница между Laravel и Slim? Отличается лишь инфраструктурный слой и способ настройки конфигурации. Slim более гибок, из-за этого разработчику нужно самому решать как настраивать своё приложение. То есть нет готовых конфигураторов или быстрого внедрения компонентов вроде бандов, как у Symfony. В Symfony и Laravel это всё есть. Не нужно тратить много времени на то, чтобы настроить что-то в данных фреймворках. Достаточно найти подходящий Brige, установить его по документации и готово. А вот в Slime не так. Приходится гибко настраивать.
У Laravel и Symfony все ответы есть в документации или же есть кучу ответов по подобным вопросам. А остальную часть используйте по урокам Дмитрия. Не вижу никаких проблем)
В Laravel слишком часто выпускают новые версии и часто меняется мода. Поэтому любые курсы конкретно по нему быстро становятся неактуальны.
Помимо этого там много упрощений и анархии в виде фасадов, трейтов, нетипизированных коллбэков и магических методов. Это удобно для разработки в стиле "фигак, фигак и в продакшен", но IDE и статические анализаторы кода сходят с ума без кучи исправляющих это плагинов. И Eloquent предполагает стиль разработки с интеграционными тестами вместо юнит-тестов и Code First.
А если рассматривать именно код приложения, то, как ответили выше, всё остальное можно внутри программировать как и здесь в Slim.
Подскажите как планируете развивать Event Driven архитектуру, а именно Domain Events?
Для взаимодействия между Агрегатами в рамках одного Контекста в DDD используется два подхода: единичная атомарная транзакция и консистентность в конечном счете.
Какой из подходов планируете использовать в аукционе?
Будете ли разделять Domain Event и Integration Event?
Будет полностью асинхронный на Domain Event.
Я всё таки ушел от того чтобы доменные события были и интеграционными и вот почему:
Хорошая статья на подобную тему тоже говорит о том, что доменные события не должны быть асинхронными и попадать в шину: https://habr.com/ru/companies/ispring/articles/569648
Вот ещё ссылка, почему доменное событие не должно быть интеграционным. В дополнении к моим пунктам: https://www.ledjonbehluli.com/posts/domain_to_integration_event/
Что-то уже 2 месяца тишина. Дмитрий..?
"Тут даже ослик сразу поймёт" (с) Винни the Пух.
Браво!
Сегодня отказался от обеда в таджикской харчевне, чтобы подписаться на месяц. Не пожалел!
Что делать, если нам надо в транзакции асинхронно обработать несколько событий. Например, создался покупатель, выдали счет, зачисили на счет 100 руб. Саги?
Либо Saga, либо компенсирующие события. Транзакций между сервисами нет. Так же если вы используете Symfony Messenger + Doctrine, то там есть Middleware for Doctrine (
doctrine_transaction
), но это на любителя.Да используем, за doctrine_transaction большое спасибо, не знал.
А ещё можно делать программные системы по аналогии с устройством биологических систем.
Добрый вечер,
Есть вопрос про так называемые политики (слушатели событий) о которых говорится в видео. На каком слое они должны быть реализованы в модуле который слушает внешние события, то есть где будет лежать этот класс этой политики в котором будет хранится связка external_event => internal_command)? На слое application (commands/queries) или на слое infrastructure (controllers/console)?
Если у вас есть деление на слои, то EventHandler лежат в слое Application, там же где CommandHandler, QueryHandler. Если нет, то в корне модуля - Auth/EventHandler.
Самый интересный кейс не разобран, когда команды нельзя связать событиями. Например, не каждый джоин пользователя должен порождать покупателя. Например, это джоин администратора. Как тогда? Кто-то должен отвечать за оркестрацию вызова всех этих команд.
Тогда покупателя можно создавать перед превой покупкой.
Как на этом сайте комментатор создаётся не сразу, а когда перед написанием первого комментария вы вписали имя в поле "Как вас называть в комментариях?" и нажали "Далее".
тема супер, когда продолжение?
Или войти через: