Практики разделения на модули и микросервисы

На декабрьском стриме мы познакомились с идеями разделения кода на модули или микросервисы и нарисовали такую штуку:

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

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

Это мы сделали на новом большом практическом стриме.

В первой части рассказали про принципы с паттернами и реализовали Transactional Outbox для надёжности отправки и доставки событий модулей или микросервисов через RabbitMQ:

  • 00:00:00 - Проверка связи
  • 00:03:00 - Сервисная архитектура бизнеса
  • 00:36:27 - Application Side Joins в контроллере
  • 01:03:20 - Если сложная аггрегация данных
  • 01:11:00 - Плюсы и минусы разделения
  • 01:19:22 - Избыточность данных сторонних сервисов
  • 01:26:42 - Надёжность очередей и атомарность операций
  • 01:35:47 - Перерыв
  • 01:51:28 - Ответы про конкурентность и Kafka
  • 02:13:58 - Чем нам полезны SOLID
  • 02:39:16 - Распределение ответственностей по GRASP
  • 02:56:04 - Для чего придумывали эти принципы
  • 03:01:20 - Избавление от связей на чужие классы
  • 03:19:32 - Полиморфные связи с разными сущностями
  • 03:28:53 - Дублирование или злоупотребления DRY
  • 03:46:47 - Чем опасно ссылаться на другую сущность
  • 03:55:10 - Перерыв
  • 04:11:48 - Ответы на вопросы
  • 04:24:18 - Проблемы транзакций в монолите
  • 04:36:36 - Саги и Eventual Consistency
  • 04:44:15 - Атомарные подоперации и PENDING-статусы
  • 04:48:16 - Хореография и оркестрация
  • 04:52:12 - Паттерн Transactional Outbox
  • 04:58:41 - Программруем очереди и Outbox
  • 05:01:08 - Генерация событий в агрегате
  • 05:09:15 - Разметка корней агрегатов
  • 05:17:19 - Установка библиотеки для AMQP
  • 05:20:44 - Установка RabbitMQ в Docker
  • 05:28:04 - Добавление модуля рассылки
  • 05:36:39 - Подключение к RabbitMQ
  • 05:40:14 - Хранилище событий
  • 05:43:40 - Автозапись доменных событий в Doctrine ORM
  • 05:55:55 - Публикация событий в очереди
  • 05:59:50 - Маршрутизация событий по exchange и очередям
  • 06:07:24 - Интерфейс Publisher очереди
  • 06:09:19 - Публикация сообщений через EventsEmitter
  • 06:11:45 - Реализация компонента Queue
  • 06:16:25 - История отправленных событий
  • 06:24:17 - Ручная доотправка по планировщику
  • 06:26:33 - Слушатель рассылки при регистрации пользователя
  • 06:29:24 - Подписка слушателей на события
  • 06:32:30 - Консольный Consumer
  • 06:43:07 - Запуск слушателя в Consumer
  • 06:48:01 - Запуск обработчиков в отдельном приложении
  • 07:00:30 - Подтверждение обработки
  • 07:07:42 - Повторная отправка истории для новых сервисов
  • 07:10:56 - Идемпотентность получателя для защиты от повторов
  • 07:21:48 - Запуск слушателя в Docker Compose и Swarm
  • 07:29:58 - Ожидание установки зависимостей
  • 07:34:35 - Отключение событий в фикстурах
  • 07:36:43 - Если общий EntityManager
  • 07:42:25 - Ответы на вопросы
  • 08:06:46 - Что рассмотрим дальше

Во второй части рассказали про разбиение существующих проектов и рассмотрели ваши вопросы:

  • 00:00:00 - Проверка связи
  • 00:02:23 - О чём сегодня поговорим
  • 00:05:43 - Утечки памяти в EntityManager
  • 00:11:16 - Подходы к обработке ошибок
  • 00:18:17 - Пинг базы данных
  • 00:20:58 - Transaction log tailing
  • 00:23:58 - Платформонезависимые имена событий
  • 00:33:13 - Чтение истории через Polling
  • 00:40:09 - Ещё про загрязнение памяти
  • 00:42:02 - Параллельное выполнение задач
  • 00:56:48 - Другие вопросы
  • 00:59:10 - От доменных исключений к событиям в сагах
  • 01:16:45 - Игнорирование последовательности слушателей
  • 01:20:48 - Потребление пямяти консьюмером
  • 01:22:16 - Другие вопросы
  • 01:25:40 - Перевод всех ошибок на события
  • 01:33:08 - Проблемы от внешних ключей
  • 01:46:12 - Другие вопросы
  • 01:49:29 - Про поддержку JSON и индексы по выражениям в БД
  • 02:08:29 - Связи ORM внутри агрегата
  • 02:11:00 - JSON или EAV в БД
  • 02:14:40 - Перерыв
  • 02:25:06 - Ответы на вопросы
  • 02:27:31 - Переупорядочивание шагов для уменьшения откатов
  • 02:38:47 - Уход от агрегатов в выборках legacy-проектов
  • 02:47:25 - Разбиение по Event Storming или Monolith First
  • 02:52:47 - Отпочковывание разросшихся сервисов
  • 02:54:08 - От ручной перелинковки до искуственного интеллекта
  • 03:22:10 - От поля для видео к сервису для медиафайлов
  • 03:37:53 - Паттерн Strangler Fig дробления монолита
  • 03:43:47 - Временный Anti-Corruption Layer
  • 03:46:40 - Замена кода через Branch By Abstraction
  • 03:59:13 - Выном модуля Newsletter в микросервис
  • 04:10:50 - Прокидывание JWT-токена аутентификации
  • 04:16:46 - Стабильность внешнего API
  • 04:22:00 - Упрощение подпроектов при разбиении
  • 04:28:06 - Независимые разработка и тестирование микросервиса
  • 04:43:08 - Эмуляция зависимостей через Wiremock
  • 05:00:08 - Эмуляция клиента и зависимости через Pact
  • 05:10:50 - Польза описания схем API
  • 05:13:32 - Версионарование внешнего и внутренних API
  • 05:21:40 - Логирование и трекинг в распределённых системах
  • 05:35:04 - Перерыв
  • 05:45:45 - Как тестировать Value Object
  • 05:49:15 - Когда использовать Traefik
  • 06:02:32 - Вопрос про права доступа в Docker
  • 06:16:40 - Как собирать цены в модуле заказа
  • 06:28:10 - Кэширование конфигурации в LaminasConfigAggregator
  • 06:32:31 - Копировать ли данные из сторонних сервисов
  • 06:33:44 - Зависимости от классов других модулей
  • 06:46:08 - Избавление от лишних хождений синхронизацией флагов
  • 06:50:51 - Нативная модульность в языке или Deptrack
  • 07:04:10 - Что если старый проект на CMS
  • 07:09:08 - Нужно ли заморачиваться производительностью
  • 07:12:45 - Переиспользовать ли Enum-ы
  • 07:13:42 - Что если команда сложная с разными вариациями
  • 07:15:53 - Формирование кастомных ответов
  • 07:19:54 - Независимость от времени создания
  • 07:24:41 - Как организзовать запросы между модулями
  • 07:26:01 - Внутренние события и интеграционные
  • 07:29:28 - Версионарование публичных событий
  • 07:30:59 - Риск выполнения групповых операций в обход команд
  • 07:39:04 - Нужна ли failure-очередь для ошибок
  • 07:43:02 - Ручная публикация без подписки на системный flush
  • 07:50:29 - Ответы на вопросы
  • 08:08:30 - Всем спасибо!

Оплатить можно самому российской или иностранной картой или попросить работодателя оплатить от имени компании.

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

А потом или сейчас приобрести записи стрима можно там же в кабинете или здесь:

Комментарии (39)
Максим (@myks92)

Супер) Стримы вообще полезная тема. Можно даже попробовать взять чью-то предметную область и собрать участников на доске. И пусть попробуют сами добавлять стикеры)

Ответить
Rus

Подскажите, этот стрим будет доступен в записи для подписчиков? Или именно эту запись нужно будет покупать отдельно?

Ответить
Дмитрий Елисеев

Да, запись будет доступна всегда.

Ответить
Rus

Отлично! С нетерпением жду выходных, чтоб посмотреть в спокойной обстановке...

Ответить
Дмитрий Елисеев

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

О дате стрима сообщу в телеграме и рассылке.

Ответить
Владимир

Спасибо за первую часть стрима, может в будущем есть смысл альтернативный интернет иметь под рукой.

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

Ответить
Здравствуйте, Дмитрий!

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

Спасибо!

Ответить
Максим (@myks92)

Организация модулей и кода никак не связана с микросервисами. В микросервисах это были бы независимые сервисы со своим независимым кодом и, возможно, на разных машинах. Но если говорим о модульном монолите, то тут вы сами должны контролировать свою архитектуру и её связанность между модулями. Для контроля архитектуры можно использовать deptrack. Кроме того можно создать модуль Shared для общих зависимостей.

Ответить
Maxim

Был бы интереснен такой кейс. Например есть заказ с позициями заказа. И в UI есть некоторое количество галочек (checkbox), типа - "Включить заказ в таможенную статистику" или - "Отслеживать кол-во упаковки в заказе" или - "Добавить адрес доставки на карту" т.е. какие-то действия, которые можно сделать с заказом "потом", но которые не являются частью модуля "Заказ".

Т.е. "Включить в таможенную статистику" должен получить позиции заказа, сгруппировать их ТНВЭД коды, и сделать какой-то отчёт для таможни. Т.е. это самостоятельное действие, где позиции заказа являются источником данных.

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

"Добавить адрес доставки на карту" - собственно понятно что делает, но тут вопрос именно в том, что создатель заказа решает, надо его добавлять или нет. И этот флажок тоже не является частью домена "Заказ".

Проставление флажков может быть как через UI в момент создания заказа. Так и с помощью какого-то фонового процесса, который бы вызвал определенную команду CreateOrderCommand модуля заказ.

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

Т.е. интересно вот что: - как в UI добавляются эти флажки. Такие бизнес требования могут добавляться с течением времени, и не хотелось бы их хардкодить. Т.е. просто к сущности Заказ можно навешивать какие-то дополнительные действия, где сам заказ является источником данных. Флажки - это для примера банальные checkbox в UI - как передавать эти флажки в команду и как их обрабатывать в command handler - как дать знать тем объектам-обработчикам, что заказ надо ещё так-то и так-то процессировать. Понятно, что можно выкинуть событие OrderCreated, но эти флажки же не являются частью доменной модели "Заказ".

Надеюсь понятно объяснил :)

Спасибо.

Ответить
Максим (@myks92)

Не очень понимаю почему это не может быть частью заказа? К примеру, добавить адрес на карту это дополнительная платная услуга. Которую нужно учитывать и оплачивать. Пример вам заказ тарифного плана у сотового оператора, где можно подключить различное количество дополнительных услуг и опций «безлимит на соцсети» и т д.

Если архитектурный вопрос, то есть два варианта решения:

  1. Добавлять все эти поля в заказ или связующую сущность в агрегате заказа и публиковать по каждому полю своё событие либо одно событие (если это связующая сущность с однотипными полями). Не факт что это не часть заказа. Заказы бывают разные и специфичные. Заказ товара в магазине это одно, а вот заказ на изготовление проектирования частного дома - совсем другое. В последнем может быть куча нюансов и дополнительных услуг. Можно просто заказать проектирование, а можно и с реализацией под ключ.
  2. Выделить отдельный сервис по созданию заказа и отдельный сервис по оформлению заказа. В первом сервисе вы будете создавать и наполнять заказ, а во втором уже оформите его и оплатите.
Ответить
slo_nik

Добрый вечер.

Вчера Вы сказали, что можно прислать примеры legacy кода для примера. Выслал Вам на почту пару маленьких архивов, приложил текстовые файлы с описанием как работает кода. Надеюсь, что он пригодится на повторном стриме и Вы дадите рекомендации как правильно работать с очередями и rabbitmq.

Ответить
ser_rid

Добрый вечер. Прошу прощения, что объемно вышло.

1) Сервисный слой возвращает избыточные данные

Условный кейс Сервисный слой возвращает массив dto|entity в контроллер. В response же должны попасть только часть полей. Где лучше разруливать этот процесс, исключая ненужные поля ? 1. В контроллере перед ответом пробегаться по всем dto и формировать из них ассоциативные массивы из нужных полей ? 2. Создать аналогичный класс dto, за исключением ненужных полей. И где то в сервисном слое написать метод, который будет перебрасывать из дто одного типа нужные поля в дто второго типа. А далее контроллер будет сразу отдавать эти дтошки, внутри которых будут только нужные данные 3. Может еще какие то варианты ...

2) Данные из разных модулей сформировать в структуру единого ответа

Условный кейс Есть модуль скиллов. Оттуда мы получаем вложенное дерево дтошек SkillDto. Есть модуль юзеров. Оттуда мы получаем массив дтошек юзеров UserDto. Есть модуль прогресса. Оттуда мы получаем массив дтошек прогрессов ProgressDto. Прогресс имеет связь с одним юзером и одним скиллом.

Для ответа на фронт нам необходимо отдать следующую структуру, как пример [

skillDto1 => [
    skillDto2 => [
        SkillDto3 => [
            UserDto1 => ProgressDto1,
            UserDto2 => ProgressDto2,
        ]
    ]
],
skillDto4 => [
    skillDto5 => [
        UserDto1 => ProgressDto3,
        UserDto2 => ProgressDto4,
    ]
], ...

]

Т.о. нам необходимо создать иерархическую структуру, включающую в себя комбинацию списков сущностей 3 разных модулей.

Какие тут наиболее удобные и наименее болезненные варианты ? 1. Дать одному модулю из 3 (например модулю прогресса) выстраивать эту иерархию ? Отдельно получить массив скиллов, юзеров, закинуть какому-нибудь сервисному классу прогресса в метод эти 2 массива, а он добавит к этому массив прогрессов, сформирует из всех 3 типов данных требуемую структуру и вернет 2. В контроллере отдельно получить массивы скиллов, юзеров, прогрессов, далее прямо в нем же сформировать требуемую иерархию. Из минусов, если требуется эту структуру отдавать не только на этом эндпойнте, но и в других местах, придется дублировать код формирования этой структуры 3. Может еще какие то варианты ...

3) Создание записи в одном модуле требует создание в другом

Условный кейс Есть модуль прогресса. Там есть сущность progress и сущность progress_log. Первое - текущий прогресс. Второе - история изменений прогресса. Есть модуль активности. Там есть сущность activity. Это - активность, выполненная для получения прогресса.

progress имеет связь с одним progress_log. А progress_log имеет связь с одной activity.

Когда создается progress, вместе с ним создаются progress_log и activity. Причем в progress_log необходимо сохранить связь с activity (activity_id), поэтому вначале необходимо создать activity, вернуть ее инкрементный ID и уже потом создать progress_log с этим activity_id. В обратную сторону аналогично, когда создается activity, то вместе с этим создаются progress_log и progress, а в этот progress_log пробрасывается ее ID.

Как тут лучше ? 1. Разруливать все эти процессы и их очередность на уровне контроллера, дергая например сначала создание activity, потом progress_log, потом progress. 2. Разруливать это все на уровне событий и слушателей. Например создаем progress, отправляем событие о создании. На событие вешается обработчик, который создает activity и progress_log (которые правда сущности разных модулей, а создаются одним обработчиком) 3. Может еще какие то варианты ...

4) Обработка событий нужна не во всех случаях Условный кейс. Есть модуль прогресса. Есть модуль активности.

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

В какой то момент нам становится необходимо в одном из участков логики создавать прогресс без активности. 1. Создавать аналог метода создания прогресса, но который не будет кидать событие ProgressCreated ? 2. Просто убрать с этого события слушателя, который создавал активность и разруливать дальше все создания аткивностей не событийным способом ? 3. Оставить событие и слушателя, но передавать в событие параметр, типа isActivityMustBeCreated ? 4. Может еще какие то варианты ...

Надеюсь примерно понятна суть сложностей. Просьба по возможности затронуть подобные вопросы) Спасибо!

Ответить
Максим (@myks92)
  1. В контроллере. При этом DTO может быть, а может не быть, – тут как захотите. Для контроля структуры можно использовать Psalm. При этом Entity я бы точно не стал использовать. У вас появится куча геттеров, которые нужны для чтения.

  2. Собирать всё на уровне контроллеров с помощью вызовов QueryHandler из разных модулей. Дублирования в этом случае будет условное. К примеру, у вас будет один и тот же Action (Controller) в api/v2 и в api/v3. Да, они похожи и может казаться, что это дубль. Но это только кажется. В какой-то момент общий сборщик придётся удалить из-за ненужно поля и тогда сервис вовсе потеряет смысл. Кроме того ваши сервисы могут жить на другой машине и общаться по api. Контроллеры и Api Gateway как раз для этого и нужно чтобы соединять всё в одном вместе.

  3. Сначала уйти от инкрементного ID. В этом случае он не особо нужен и в будущем будет проще. Но если хотите, то можете оставить и так. Создавайте его сами. В Activity добавьте поля entity_type и entity_id. Затем по событию ProgressLogAdded создавайте Activity где entity_type=progress_log, а entity_id=progress_lig_id.

4.Здесь несколько вариантов:

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

4.2 Второй подход заключается в том, что публикуется специальное событие для этого модуля. Например, ProccessWithAcivityCreated. Тогда нам без разницы что внутри этого события. Нам потребуется только ID прогресса, чтобы создать у себя активность. С выделением отдельного события нам не нужно беспокоиться об изменении полей события ProccessCreated.

Что касается нужен ли фабричный метод сложно сказать. Нужно понимать контекст. Скорее всего, в вашем случае, будет достаточно одного создания через конструктор с публикацией второго события по if($activity ===true)

Ответить
Сергей

Как организованы query-запросы между модулями? Чтобы не инжектить хендлеры одних модулей в другие. Прямо сетевыми http-запросами? или через QueryBus и последующий разбор $envelope? или как-то еще?

Ответить
Максим (@myks92)

00:36:27 - Application Side Joins в контроллере Можете через QueryBus, но она должна быть синхронной.

Ответить
Сергей

Я имею ввиду, что из себя представляют эти апишки, которые вызываются в указанной части ролика? Вот FavoritesApi - это что? Где этот класс хранится? И если он хранится в вызывающем модуле, то как он связан с модулем Favorites?

Ответить
Максим (@myks92)

FavoritesApi - это пример Http клиента, который написан для сервиса не в монолите или вообще сервис сторонний. Когда ваш код не в монолите, а в разных приложениях они могут взаимодействовать по API. Для них и пишутся такие клиенты. Клиенты можно вынести в отдельную библиотеку, если пользоваться этой библиотекой PHP будете не вы одни. Если вы одни, то можете не выносить в отдельную библиотеку, а положить в своём монолите.

Если у вас модуль Favorites находится в монолите и доступен в этой же кодовой базе - делать для него FavoritesApiClient и обращаться через HTTP не нужно. Просто вызывается Fetcher напрямую или через QueryBus. Тут как Вам угодно.

Ответить
Сергей

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

Ответить
Сергей

Fetcher - это видимо и есть класс, возвращающий результат какого-то запроса? И он лежит внутри модуля, например Favorites. Если, например, админский модуль хочет получить все избранные записи, он обращается к этому фетчеру напрямую?

Ответить
Максим (@myks92)

Я Вам отвечаю на вопросы согласно кода Дмитрия: https://github.com/deworkerpro/demo-auction/tree/master/api. Если у Вас так же, то папка HTTP является Api Gateway. Он как раз служит для того, чтобы склеивать информацию с разных модулей и даже сторонних сервисов. Поэтому у вас тут связанности между модулями нет. Api Gateway как дирижёр над вашими модулями. А вот если у вас контроллеры внутри каждого модуля, тогда да. У вас от связанности не уйти в таком случае. Я бы рекомендовал сделать папку Http или ApiGateway, которая как раз и будет связывать данные из ваших модулей. Лично я делаю вместо Fetcher Handler, который работает через QueryBus синхронно. Это позволяет в контроллерах подключать только QueryBus без Handler. Меньше кода и зависимостей. Но вызов всё равно не в модулях, а в Api Gateway

Ответить
Сергей

Спасибо за ответ. То, что нужно)

Ответить
Сергей

Кстати, репозиторий из трансляции недоступен? В аукционе сейчас вижу только модуль аутентификации и все

Ответить
Максим (@myks92)

Репозиторий, который показывался на стриме - это реальный код сайта deworker.pro. Его не публикуют по понятным причинам. Поэтому код есть только Аукциона. Он тоже со временем приобретёт такой же вид, но позже. Возможно Дмитрий выложит и код сайта deworker.pro, как он сделал это с elisdn.ru (https://github.com/elisdnru/site). Но, думаю, не в ближайшем будущем)

Ответить
Сергей

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

Ответить
Максим (@myks92)
  1. Смотря что вы подразумеваете под словом Админка. В большинстве случаев это вообще как агрегатор нескольких модулей. У каждого модуля свои таблицы, поэтому она не имеет своих таблиц. Но в другом контексте могут быть и свои таблицы. Например, Администратор/Модератор комментариев. А если это всем привычная Админка, то тут в ApiGateway просто будут свои контроллеры по адресу api/admin/users, api/admin/shop и так далее. Отличие от других контроллеров в выдаче данных и другие права доступа и возможно какие-то команды другие, но они будут жить каждый в своём модуле, а не модуле админ. Такой папки в src у вас не будет.
  2. Когда требуется сложная сортировка в Админке из разных модулей можно сделать свой сервис и все данные объединить туда. Я обычно делаю сервис уже не на таблицах, а на индексах в ElasticSearch. Он более приспособлен для всяких агрегаций сложных фильтраций и поиска. Но такие объединения нужны не всегда и их нужно будет поддерживать. Например, переиндексировать.
Ответить
Юрий

Стрим доступен всем?

Ответить
Дмитрий Елисеев

Доступен всем купившим.

Ответить
Юрий

по ссылке можно так перейти.

Ответить
Дмитрий Елисеев

Да, это специфика YouTube.

Ответить
Максим (@myks92)
  1. Почему Вы полноценно не используете CommandBus и QueryBus ($this->commandBus->dispatch($command)), чтобы в нём уже запускать Validator ваших DTO. Вместо этого Вы напрямую вызываете $this->validator->validate($command) и $this->handler->handle($command). Из-за этого возникает постоянная необходимость в EventHandler, Action добавлять всегда две зависимости: Handler и Validator.
  2. Вопрос уже задавал по событиям, но ещё раз акцентирую своё внимание к интеграционным событиям. 2.1 У Вас все доменные события являются интеграционными. Как в таком случае поступать с изменением структуры событий? Завтра поменяется поле в сущности или роль Пользователя станет массивом, то нарушится контракт события. Придётся всем доменным событиям всегда применять версионирование, потому что мы не всегда можем знать есть ли слушатель у нашего события или нет... 2.2 Что если нам нужно в доменные события добавлять дополнительную информацию, которую мы не можем взять из нашего Агрегата, а можем взять только из соседнего агрегата? 2.3 Что если нам нужно публиковать событие по условию, которое можем узнать только сделав запрос в базу. Например, при создании пользователя нам нужно отправить событие о регистрации каждого 1000-го пользователя. На это событие подпишется какой-то сервис подарков и выдаст бесплатную подписку на 1 год. 2.4 Если мы настроим какой-то импорт пользователей из сторонней системы, то у нас будет куча событий о добавлении пользователей. Теперь, чтобы в сервисе статистики поменять количество пользователей мне нужно слушать каждый раз событие о добавлении, вместо того чтобы отправить одно событие, что импортировано 10 000 пользователей, которое в сервисе статистики добавит к имеющемуся числу 10 000, вместо того, чтобы каждый раз увеличивать число на +1 и так 10 000 раз. Пример условный и упрощенный.
  3. Если какие-то события имеют ошибку, которая поправима только после релиза новой версии, то нам надо отложить события в очередь failture, а после исправления ошибки снова опубликовать в основную очередь. Или другая политика ретраев. Как в этом случае быть? Например, у Symfony такое реализовано.
  4. Чтобы в тестах везде не делать $user->realizeEvents() можно отказаться от DoctrineSubscriber и сохранять в EventStore события из нашего Flusher. Для этого Flusher сделать в виде интерфейса FlusherInterface и сделать разные реализации DoctrineFlusher, EventFlusher, AllFlusher и т д. Теперь избавляемся от подписчика и для прода используем FlusherInterface::class => new AllFlusher([DoctrineFlusher::class, EventFlusher::class]), а для тестов FlusherInterface::class => DoctrineFlusher::class. И в тестах менять ничего не придётся. А так получается очень странно, что мы из-за какого-то доктриновского подписчика будем переписывать все тесты добавляя в каждый тест $user->realizeEvents(). Представим что у нас уже готовый проект и много тестов. Выглядит не очень...
  5. Будете ли реализовывать оптимистическую блокировку? Там есть проблема с вложенными сущностями. Приходиться добавлять агрерирующее поле. Решил добавлением поля aggregeteVersion.
Ответить
Максим (@myks92)

По 1 пункту увидел, что в планах перевести на CommandBus. Поэтому можно не отвечать.

Но появился другой вопрос. Какой смысл от «проигрывания» всех прошлых событий, если при любом перемещении или переименования события и слушателя вся система ломается. Например. Переместили подписчик в другое место и идемпотентность уже не работает. Для него это выглядит как новый слушатель. Конечно, вы говорили, что можно использовать свои названия, вместо названий классов, тогда почему это не сделать сразу? Ведь проблема не застанет долго ждать. Либо тогда придётся выдумывать карту алиасов.

Ответить
Артём

Эх, Дмитрий уже не тот. Где фирменное рисование схем символами в ide?

Ответить
Максим

Добрый день! Хочу прояснить, правильно ли понял все по поводу модульного монолита. На проекте выделено три модуля:

  • Заказы
  • Товары
  • API (api gateway)

Для создания заказа модулю заказов нужны некоторые данные о товарах (для примера не важно какие, дело именно в том, что данные как раз в другом модуле). Для того, чтобы избавиться от связей между модулями делаем так:

  1. Запрос клиента "Создай заказ" поступает на модуль API.
  2. Модуль API получает нужные данные из модуля "Товары"
  3. Модуль API вызывает команду "Создай заказ" модуля "Заказы"

Все ли верно ?

Ответить
Дмитрий Елисеев

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

Ответить
Максим

Вы случайно не про разсетку корней агрегатов ?))

В примере, как описал выше, получается "толстый контроллер". По простому : 1) создали контроллер(обслуживает рест запросы) 2) пришел 1ый запрос - пошли в один модуль, потом в другой, третий и т.д 3) собрали кучу параметров и только потом пошли в конечный модуль,который выполнит основную работу

Ответить
Максим

А, нашел "Как собирать цены в модуле заказа"

Ответить
Артём

Дмитрий, почему бы вам не попробовать для рисования схем использовать опен сурсный https://excalidraw.com/

Ответить
Максим

Добрый вечер! Подскажите, пжл, почему консюмеров событий не запускать по cron? Встречал на практике, создано в проекте порядка 40 разных очередей и такое же количество консюмеров запускается по cron при этом количество воркеров настраивалось командой sleep, и вроде живут. В чем особенность такого подхода?

Ответить
Зарегистрируйтесь или войдите чтобы оставить комментарий

Или войти через:

Yandex
MailRu
GitHub
Google