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

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

Но что это такое? Когда и как его использовать? Какие есть минусы?

Что / Когда / Почему

Как и классы, компоненты должны иметь низкую связь между собой, но быть внутри них высокой связностью. Когда компоненты должны взаимодействовать друг с другом, допустим, компонент «A» должен запускать некоторую логику в компоненте «B», естественный способ сделать это - просто заставить компонент A вызвать метод в объекте компонента B. Однако, если A знает о существовании B, они связаны, A зависит от B, что затрудняет изменение и сопровождение системы. События можно использовать для предотвращения связи.

Кроме того, в качестве побочного эффекта использования событий и разделения компонентов, если у нас есть команда, работающая только над компонентом B, это может изменить то, как компонент B реагирует на логику в компоненте A, даже не разговаривая с командой, ответственной за компонент A. Компоненты могут развиваться независимо: наше приложение становится более органичным.

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

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

Чтобы не превратить нашу кодовую базу в большую кучу спагетти-кода, мы должны ограничить использование событий четко определенными ситуациями. По моему опыту, есть три случая использования событий:

  1. Чтобы разъединить компоненты
  2. Для выполнения асинхронных задач
  3. Для отслеживания изменений состояния (журнал аудита)

1. Чтобы разъединить компоненты

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

Это означает, что и A, и B будут зависеть от диспетчера и события, но они не будут знать друг друга: они будут разделены.

В идеале и диспетчер, и событие не должны находиться ни в одном из компонентов:

  • Диспетчер должен быть библиотекой, полностью независимой от нашего приложения и, следовательно, установленной в общем месте с использованием системы управления зависимостями. В мире PHP это что-то установленное в папке vendor с помощью Composer.
  • Тем не менее событие является частью нашего приложения, но должно существовать вне обоих компонентов, чтобы они не знали друг друга. Событие разделяется между компонентами и является частью ядра приложения. События являются частью того, что DDD называет общим ядром. Таким образом, оба компонента будут зависеть от общего ядра, но не будут знать друг друга.
    Тем не менее, в монолитном приложении для удобства допустимо разместить его в компоненте, который запускает событие.

Общее ядро ​​

[…] Обозначьте явными границами какое-то подмножество модели предметной области, которое команды соглашаются совместно использовать. Держите это ядро ​​маленьким. […] Этот явно общий доступ имеет особый статус, и его нельзя изменять без консультации с другой командой.

Эрик Эванс, 2014 г., Справочник по доменно-ориентированному дизайну

2. Для выполнения асинхронных задач

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

Например, размещение заказа в интернет-магазине может выполняться синхронно, но отправка электронного письма с уведомлением пользователя может выполняться асинхронно.

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

В этих случаях на самом деле не имеет значения, находится ли связанная логика в одном и том же ограниченном контексте или нет, в любом случае логика разделена.

3. Для отслеживания изменений состояния (журнал аудита)

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

Проблема здесь в том, что мы не храним точно, что и когда изменилось.

Мы можем хранить события, содержащие изменения, в виде конструкции журнала аудита.

Подробнее об этом позже, в объяснении о Event Sourcing.

Узоры

Мартин Фаулер выделяет три различных типа паттернов событий:

  • Уведомление о событии
  • Перенос состояния при событии
  • Event-Sourcing

Все эти шаблоны используют одни и те же ключевые концепции:

  1. События сообщают, что что-то произошло (они происходят после чего-то);
  2. События транслируются в любой код, который прослушивает (несколько единиц кода могут реагировать на событие).

Уведомление о событии

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

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

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

Преимущества

  • Повышенная устойчивость, если события помещены в очередь, компонент-источник может выполнять свою логику, даже если вторичная логика не может быть выполнена в этот момент из-за ошибки (поскольку они поставлены в очередь, они могут быть выполнены позже, когда ошибка будет исправлена);
  • Сниженная задержка: если событие поставлено в очередь, пользователю не нужно ждать выполнения этой логики;
  • Команды могут развивать компоненты независимо, делая свою работу проще, быстрее, менее подверженной проблемам и более органичной;

Недостатки

  • Если его использовать без критериев, он может превратить кодовую базу в груду спагетти-кода.

Перенос состояния при событии

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

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

Преимущества

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

Недостатки

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

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

Поиск событий

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

Журнал транзакций

Это нормально для большинства случаев, но что произойдет, если нам нужно знать, как Сущность достигла этого состояния (т. Е. Мы хотим знать кредиты и дебиты нашего банковского счета)? Это невозможно, потому что мы храним только текущее состояние!

Используя источник событий, вместо сохранения состояния объекта мы фокусируемся на сохранении изменений состояния объекта и вычисления состояния объекта на основе этих изменений. Каждое изменение состояния - это событие, хранящееся в потоке событий (т. Е. В таблице в СУБД). Когда нам нужно текущее состояние объекта, мы вычисляем его по всем его событиям в потоке событий.

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

Грег Янг 2010, Документы CQRS

Удаления

Если у нас есть изменение состояния (событие), которое является ошибкой, мы не можем просто удалить это событие, потому что это изменит историю изменения состояния, и это будет противоречить самой идее поиска источника событий. Вместо этого мы создаем событие в потоке событий, которое отменяет событие, которое мы хотели бы удалить. Этот процесс называется Reversal Transaction, и он не только возвращает Entity в желаемое состояние, но также оставляет след, который показывает, что объект находился в этом состоянии в данный момент времени.

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

Грег Янг 2010, Документы CQRS

Снимки

Однако, когда у нас много событий в потоке событий, вычисление состояния Entity будет дорогостоящим и неэффективным. Чтобы решить эту проблему, каждые X событий мы будем создавать моментальный снимок состояния Entity в этот момент времени. Таким образом, когда нам нужно состояние объекта, нам нужно только вычислить его до последнего снимка. Черт, мы даже можем сохранить постоянно обновляемый снимок Сущности, таким образом у нас есть лучшее из обоих миров.

Прогнозы

При поиске событий у нас также есть концепция проекции, которая представляет собой вычисление событий в потоке событий, начиная с определенных моментов и до них. Это означает, что снимок или текущее состояние объекта соответствует определению проекции. Но самая ценная идея в концепции прогнозов состоит в том, что мы можем анализировать «поведение» Сущностей в определенные периоды времени, что позволяет нам делать обоснованные предположения о будущем (т. Е. Если в последние 5 лет Сущность имела повышенная активность в течение августа, вполне вероятно, что в августе следующего года произойдет то же самое), и это может быть чрезвычайно ценной возможностью для бизнеса.

Плюсы и минусы

Поиск событий может быть очень полезен как для бизнеса, так и для процесса разработки:

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

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

  • Внешние обновления
    Когда наши события запускают обновления во внешних системах, мы не хотим повторно запускать эти события, когда мы воспроизводим события, чтобы создать проекцию. На этом этапе мы можем просто отключить внешние обновления, когда мы находимся в «режиме воспроизведения», возможно, инкапсулируя эту логику в шлюзе.
    Другое решение, в зависимости от реальной проблемы, может заключаться в буферизации обновлений во внешних системах , выполняя их через определенное время, когда можно с уверенностью предположить, что события не будут воспроизведены.
  • Внешние запросы
    Когда наши события используют запрос к внешней системе, т. е. Получая рейтинги фондовых облигаций, что происходит, когда мы воспроизводим события, чтобы создать прогноз? Возможно, мы захотим получить те же рейтинги, которые использовались при первом запуске событий, может быть, много лет назад. Таким образом, либо удаленное приложение может предоставить нам эти значения, либо нам нужно сохранить их в нашей системе, чтобы мы могли имитировать удаленный запрос, опять же, инкапсулируя эту логику в шлюзе.
  • Изменения кода
    Мартин Фаулер выделяет 3 типа изменений кода: новые функции, исправления ошибок и временная логика . Настоящая проблема возникает при воспроизведении событий, которые должны воспроизводиться с разными правилами бизнес-логики в разные моменты времени, т.е. налоговые расчеты в прошлом году отличаются от расчетов в этом году. Как обычно, можно использовать условную логику, но она станет беспорядочной, поэтому советуем использовать вместо нее шаблон стратегии.

Так что я советую соблюдать осторожность и по возможности следую этим правилам:

  • Держите события в тупике, зная только об изменении состояния, а не о том, как оно было решено. Таким образом, мы можем безопасно воспроизвести любое событие и ожидать, что результат будет таким же, даже если бизнес-правила тем временем изменились (хотя нам нужно будет сохранить устаревшие бизнес-правила, чтобы мы могли применять их при воспроизведении прошлых событий);
  • Взаимодействие с внешними системами не должно зависеть от этих событий, таким образом мы можем безопасно воспроизвести события без опасности повторного запуска внешней логики, и нам не нужно гарантировать, что ответ от внешней системы будет таким же, как при исходном воспроизведении события.

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

Вывод

Опять же, в основном речь идет об инкапсуляции, низкой связи и высокой когезии.

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

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

Источники

2005 • Мартин Фаулер • Event Sourcing

2006 • Мартин Фаулер • В центре внимания события

2010 • Грег Янг • Документы CQRS

2014 • Грег Янг • CQRS и Event Sourcing - Code on the Beach 2014

2014 • Эрик Эванс • Справочник по доменно-ориентированному дизайну

2017 • Мартин Фаулер • Что вы имеете в виду под« событийно-ориентированным

2017 • Мартин Фаулер • Многие значения событийной архитектуры

Первоначально опубликовано на сайте herbertograca.com 5 октября 2017 г.