Comments 7
Спасибо, @slayervc за поднятие темы. Согласен, что сообщение это не событие, а Concurrency is not parallelism, да и вообще странно видеть два этих понятия в одном сравнении. Сообщение шлем кому-то о чем то. Событие "бросаем", и о получении речи не идет.
Другой вопрос - разговорная практика. Кто-то надевает рубашку, кто-то одевает, да, один филолог умер в таком примере, но, в любом случае, о чем идет речь - понятно.
Так и тут: я могу отправить сообщение о том, что произошло событие и могу включить атрибуты события в состав message. Или могу сказать "отправить событие", да еще один филолог умер, но суть понятна. я отправил сообщение с событием в теле. И это понятно и быстро иии .... в статье именно эта устная форма и используется:
Основная мысль здесь состоит в том, что вы сначала определяетесь с тем, что вы отправляете (событие, документ, команду)
И если душнить, то вообще-то отправляем мы сообщение. В сообщение положим не событие, а тело объекта события и т.д. Но даже в тексте проще было сказать: "отправляем событие". И всем всё ясно... Или нет.
Короче, это почти как в том анекдоте:
Учительница: — Я уже сотый раз объясняю, что половина не может быть большей или меньшей! А большая половина класса этого так и не понимает!.
Спасибо за Ваш комментарий.) Я потому и поставил уровень статьи "легкий".) Если человек понимает разницу, то ему статья с объяснением этой разницы будет бесполезна.) Но беда в том, что я встречал очень многих разработчиков, которые этой разницы не понимают, и это воплощалось в их коде.
Возможно, я слишком большой акцент сделал на различии понятий и слишком малый - на архитектуре. Идея-то была показать, где, в каком слое, в проекте с хорошей архитектурой место события, где - его обработчиков, а где - транспорта.
Основная мысль, которая, возможно, утонула в лингвистически разборах, о которых Вы говорите, состояла в том, что доменное событие, прежде чем дойти до внешнего консьюмера, проходит длинный путь преобразования: Доменное событие -> Domain (!) Event Dispatcher -> Обработчик -> Message (перзистентное) -> Relay -> сериалайзер Symfony Messenger -> Транспорт Symfony Messenger -> Rabbit MQ и далее в консьюмере обратный путь через десереализацию к хандлеру, который уже совсем не доменный.
Это, на самом деле, первая часть большой статьи, которую я решил разделить. Спасибо, что подсветили, во второй части сделаю прям больший упор на архитектуру, код и путь движения события через слои.
Но беда в том, что я встречал очень многих разработчиков
К сожалению это происходит очень часто. В итоге люди путают семантику этих двух понятий и в следствии этого порождаются очень странные архитектурные решение, происходит сильное кросс сервисное/контекстное зацепление (где его не должно быть) и тд.
Мне кажется важно запомнить / понимать базовые отличия, чтобы успешно оперировать событиями и сообщениями (job-ами) в архитектуре приложения.
Событие
Говорит о том что УЖЕ случилось в прошлом
Паблишер событий НИКОГДА не знает о обработчиках событий
Обработчиков может быть от 0 до бесконечности
Как правило служит для организации кросс контекстного / сервисного взаимодействия
Сообщение (job-а)
Служит обычным средством распараллеливания / асинхронного выполнения тех или иных операций в приложении
Постановка задачи в будущем
Паблишер как правило знает о обработчике событий
Обработчик как правило один
Ну и да, событие - это дефакто сообщение в шине, но имеющее иную смысловую нагрузку нежели просто job-а, а также событие и job-а не должны пересекаться в одном транспорте.
handlers
,listeners
,subscribers
, кому как угодно
Handler — это синхронный обработчик, способный вернуть значение, которое может повлиять на дальнейшее выполнение. Нуждается в регистрации. Доставка сообщения гарантируется.
Listener — это синхронный, либо асинхронный обработчик, не способный повлиять на control flow. Нуждается в регистрации. Доставка сообщения гарантируется. Такой себе read-only handler.
Subscriber — это асинхронный обработчик в модели pub/sub. Не нуждается в регистрации, достаточно подписки. Доставка сообщения не гарантируется.
———
Взаимодействие с другим приложением (или другим куском того же приложения, или сразу несколькими приложениями), посредством сообщений, называется акторная модель. Сообщения всегда асинхронные (но возможна искусственная костылеобразная абстракция, делающая их синхронными для вызывающего объекта).
Говорить «отправили сообщение» про взаимодействие с handlers и listeners — не сто́ит, потому что это обычно запутает людей в теме.
Ну и так далее.
Теперь мы можем писать различные обработчики для события
UserCreated
. Например, обработчик отправляющий в telegram приветствие нового пользователя
В примере событие брошено в конструкторе объекта, при таком сценарии мы отправим приветствие в телеграм, даже если пользователя не удалось сохранить в базу данных.
Как выглядит обработка таких сценариев?
То есть в теории это красиво и логично, но на практике событие "юзер создан" это всё-таки его успешное сохрание куда-то, а не конструкция объекта.
Очень хороший вопрос. Вам нужен паттерн "Транзакционный обмен сообщениями" Криса Ричардсона https://microservices.io/patterns/data/transactional-outbox.html.
Коротко: на все ваши события подписан обработчик, который сохраняет их в базу. В Application слое вызов бизнес-логики, бросающей события, обернут в одну транзакцию с вызовом метода save() репозитория. Так вы гарантируете атомарность выполнения действия и выброса события. Отдельный класс `Message Relay` вытаскивает сохраненные события из базы и дальше оборачивает их в сообщение и отправляет асинхронный транспорт (тот же раббит). Уже консьюмер сообщения рабита вытаскивает из него событие и асинхронно обрабатывает его.
Эта статья - первая часть большой статьи про Messenger, которую я решил поделить. Во второй части я подробно на примере покажу, как работает транзакционный обмен сообщениями.
События vs сообщения. Понимаете ли вы разницу и почему это важно?