handbookНаписать

События вместо прямых вызовов (Events instead of calls) ​

Оформление заказа перестаёт звать почту, склад и аналитику напрямую: оно сообщает "заказ оформлен", а каждая реакция подписывается сама и падает в одиночку.

Урок - пошаговый рефакторинг. Каждый шаг - отдельная рабочая копия кода на PHP, Go, Python и TypeScript со своими тестами. Тесты по смыслу одинаковы во всех шагах, поэтому видно, что меняется устройство, а не результат. Одно исключение сделано намеренно, о нём ниже.

Исходная задача ​

Покупатель оплатил заказ. Checkout.place сохраняет его, и дальше должны произойти три вещи: склад ставит заказ в сборку, аналитика получает событие order_placed с суммой, покупателю уходит письмо "Order 42 confirmed". Почта, склад и аналитика в уроке - фейки, которые записывают вызовы в списки. У аналитики есть флаг down: внешний сервис лежит, и вызов падает с ошибкой "analytics is down".

Что болит ​

  • Checkout знает обо всех, кто реагирует на заказ. Новая реакция (бонусные баллы, вебхук партнёру) - правка кассы и её конструктора.
  • Чтобы протестировать оформление, нужно собрать все три зависимости, даже если тест о другом.
  • Лёг сервис аналитики - покупатель не получил письмо, а place вернул ошибку, хотя заказ уже сохранён. Самая неважная реакция ломает самую важную.

Шаги ​

  1. Прямые вызовы - исходный код: Checkout получает Warehouse, Analytics, Mailer и зовёт их по очереди. Тесты фиксируют поведение, включая боль: упавшая аналитика роняет place, письма нет.
  2. Доменное событие и синхронный диспетчер - Checkout публикует OrderPlaced через EventDispatcher, реакции стали слушателями RequestPicking, TrackPurchase, SendConfirmation. Касса больше не знает о почте, но ошибка слушателя по-прежнему прерывает всё.
  3. Независимые подписчики - диспетчер ловит ошибку каждого слушателя, отдаёт её в report и зовёт следующих. place не падает, письмо уходит, сбой виден в отчёте.

Что меняется в тестах ​

Первый тест одинаков во всех шагах: place сохраняет заказ и запускает все три реакции. Второй тест - сценарий с лежащей аналитикой. В шагах 1 и 2 он фиксирует текущую боль: ошибка долетает до place, письма нет. В шаге 3 тот же сценарий утверждает обратное: письмо ушло, сборка запрошена, ошибка попала в отчёт. Это единственное намеренное изменение поведения в уроке, остальные тесты не менялись. В шагах 2 и 3 добавлен тест, что касса работает и без единого подписчика.

Итог ​

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

Граница у этого решения есть. Диспетчер синхронный и живёт в одном процессе: если процесс упадёт между сохранением заказа и рассылкой, письма не будет, и повторить его некому. Когда реакция обязана случиться после коммита (оплата, сборка), событие пишут в ту же транзакцию, что и заказ, и рассылают отдельным процессом - это Outbox. А то, без чего заказ не может считаться оформленным (резерв товара, списание денег), остаётся прямым вызовом внутри операции, а не подписчиком.

Связанные темы ​

  • Доменное событие (Domain Event) - факт в прошедшем времени, на который реагируют другие части системы
  • Наблюдатель (Observer) - подписка без знания о подписчиках, основа диспетчера
  • Outbox - доставка событий после коммита, когда синхронного диспетчера мало
© 2014-2026 Тимур ТурдыевСайт и справочник работают на VPS от AdminVPS