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

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

Event Sourcing - это концепция от Domain-Driven Design. Об этих темах написано много, но вкратце, в разделе Источники событий история событий сохраняется в хранилище событий, а текущее состояние приложения определяется редукторами (чистыми функциями, которые принимают историю событий в качестве входных данных). Состояние приложения полученный в результате запуска редукторов в истории событий, называется агрегатом.

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

Обычно преимущества поиска событий лучше всего реализуются для основного домена вашего приложения (основная проблема, которую оно пытается решить) и для построения масштабируемой архитектуры CQRS, поэтому я был удивлен, увидев, что он появился в популярном интерфейсе. технология. Пользователи React + Redux пользуются шаблоном источников событий для предоставляемых им средств отладки, таких как возможность проверять историю событий и перематывать состояние приложения.

Redux

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

Утверждается, что автор Redux, Дэн Абрамов, даже не слышал о доменно-ориентированном дизайне, когда создавал его. Тем не менее, в официальной документации Redux говорится, что действия должны быть разработаны как события (выделено ими):

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

Далее они предоставляют список примеров событий в прошедшем времени:

  • {type: 'todos/todoAdded', payload: todoText}
  • {type: 'todos/todoToggled', payload: todoId}
  • {type: 'todos/colorSelected, payload: {todoId, color}}
  • {type: 'todos/todoDeleted', payload: todoId}
  • {type: 'todos/allCompleted'}
  • {type: 'todos/completedCleared'}
  • {type: 'filters/statusFilterChanged', payload: filterValue}
  • {type: 'filters/colorFilterChanged', payload: {color, changeType}}

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

Антипаттерн Redux

Другие тоже заметили это несоответствие. Язан Алабуди создал презентацию под названием Наша антипаттерн Redux, в которой он рассказывает о ловушке разработки действий Redux, которые слишком много предполагают состояние, в котором они собираются мутировать, эффективно превращая Redux в шину сообщений pub-sub, где издатели тесно связаны с подписчиками.

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

Один из способов реализовать это - иметь действие для каждого из этих изменений состояния, и когда цель забита, каждое из этих действий отправляется:

  • addTeamGoals(scoringTeam, +1)
  • addPlayerGoals(scoringPlayer, +1)
  • addGoalieGoalsAllowed(goalieInNet, +1)
  • setGameState('faceoff')
  • и т.п.

Лучше всего сделать это одним действием, фиксирующим событие: goalScored(scoringTeam, scoringPlayer, goalieInNet) и иметь отдельные редукторы для каждого из соответствующих изменений состояния. Это создает разделение между издателями событий и подписчиками, так что эффекты, которые имеют события, могут быть обновлены без необходимости касаться кода, который их публикует.

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

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

Средство

Я заметил, что тот же антипаттерн проявляется и в проекте, над которым я работал здесь, в Tableau, в течение прошлого года. Команда, в которой я работаю, начала разрабатывать некоторые из наших действий Redux с такими именами, как changeStage и setErrorType, где мы использовали текущий этап и / или текущую ошибку, чтобы решить, какой компонент React визуализировать.

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

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

Наша команда поймала бы этот архитектурный сбой раньше, если бы мы называли свои действия в прошедшем времени. Вызов действия stageChanged кажется неправильным, когда именно акт отправки действия изменяет сцену. Один из членов нашей команды даже указал на это в обзоре кода, когда было предложено переименовать changeStage в stageChanged. Решение состоит не в том, чтобы переименовать действие, а в том, чтобы полностью заменить его другими действиями, в которых имя в прошедшем времени имеет смысл. Почему поменялась сцена? Потому что была нажата кнопка.

Заключительные замечания

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

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

Для дальнейшего чтения рассмотрите следующие ресурсы: