Как моделировать поведение программ Redux с помощью диаграмм состояния

1656682938 kak modelirovat povedenie programm redux s pomoshhyu diagramm sostoyaniya

Лука Маттейс

PBrhox4dYsnCIBxDqWKaqLz4rEhf7MVfpWyq
Диаграммы состояний: визуальный формализм для сложных систем

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

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

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

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

Redux и диаграммы состояний

Redux достаточно прост. У нас есть компонент пользовательского интерфейса, который запускает событие. Мы тогда dispatch действие, когда происходит это событие. Редуктор использует это действие для обновления хранилища. Наконец наш компонент поступает непосредственно из обновлений в магазин:

// our UI componentfunction Counter({ currentCount, onPlusClick }) {  return <>    <button onClick={onPlusClick}>plus</button>    {currentCount}  <>}
// let's connect the component to reduxconnect(  state => ({ currentCount: state.currentCount }),  dispatch => ({     onPlusClick: () => dispatch({ type: INCREMENT })  }))(Counter)
// handle the INCREMENT update using a reducerfunction currentCountReducer(state = 0, action) {  switch(action.type) {    case INCREMENT:      return state + 1;    default:      return state;  }}

Это почти все, что касается Redux.

j8cLfILp7OZ9z99gm6-JM6m5FGciCZD3bBJh

Чтобы представить диаграммы состояний вместо сопоставления нашего события непосредственно с действием обновления, мы сопоставляем его с общим действием, которое не обновляет никаких данных (ни один редуктор не обрабатывает это):

// currently we are mapping our event to the update:// onPlusClick -> INCREMENT// instead, we dispatch a generic event which is not an update:// onPlusClick -> CLICKED_PLUS // this way we decouple our container from knowing // which update will happen.// the statechart will take care of triggering the correct update.
connect(  state => ({ currentCount: state.currentCount }),  dispatch => ({     onPlusClick: () => dispatch({ type: CLICKED_PLUS })  }))(Counter)

Без ручек редуктора CLICKED_PLUS поэтому мы позволили это обрабатывать диаграмме состояний:

const statechart = {  initial: 'Init',  states: {    Init: {      on: { CLICKED_PLUS: 'Increment' }    },    Increment: {      onEntry: INCREMENT, // <- update when we enter this state      on: { CLICKED_PLUS: 'Increment' }    }  }}

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

В приведенном выше примере мы начинаем с того, что находимся в Init состояние. Когда CLICKED_PLUS происходит событие, мы переходим к Increment государство, имеющее ан onEntry поле. Это делает отправку диаграммы состояний anINCREMENT действие — на этот раз обрабатывается редуктором, обновляющим хранилище.

Вы можете спросить, почему мы отключили контейнер от информации об обновлении? Мы сделали это так, чтобы все поведение относительно того, когда должно происходить обновление, содержалось в структуре JSON диаграммы состояний. Это значит, что его можно визуализировать:

f20MvpyzWylXFxR739RS4UVAxLVG1pKslHL8

Это может привести к улучшению поведения нашей программы, просто изменив JSON описание диаграммы состояний. Давайте улучшим наш дизайн, сгруппировав дваCLICKED_PLUS переходит в одно, используя концепцию иерархических состояний:

0XUooK1P2XVwPZHIXAq0AdctW5wcBzCg6PMx

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

{  initial: 'Init',  states: {    Init: {      on: { CLICKED_PLUS: 'Init.Increment' },      states: {        Increment: {          onEntry: INCREMENT        }      }    }  }}

Асинхронные побочные эффекты

Представим, что когда a <FetchDataButton /> мы хотим начать HTTP-запрос. Вот как мы сейчас это делаем в Redux без диаграмм состояния:

connect(  null,  dispatch => ({     onFetchDataClick: () => dispatch({ type: FETCH_DATA_CLICKED })  }))(FetchDataButton)

Тогда мы, наверное, имели бы эпопею, чтобы справиться с таким действием. Ниже мы используем redux-observable, но также можно использовать redux-saga или redux-thunk:

function handleFetchDataClicked(action$, store) {  return action$.ofType('FETCH_DATA_CLICKED')    .mergeMap(action =>      ajax('        .mapTo({ type: 'FETCH_DATA_SUCCESS' })        .takeUntil(action$.ofType('FETCH_DATA_CANCEL'))    )}

Несмотря на то, что мы отделили контейнер от побочного эффекта (контейнер просто сообщает эпопею «привет, кнопка получения данных была нажата»), у нас все еще есть проблема, что HTTP-запрос запускается независимо от того, в каком состоянии мы находимся. .

Что, если мы находимся в состоянии, где FETCH_DATA_CLICKED не должен запускать запрос HTTP?

Этот случай легко разрешим с помощью диаграмм состояния. Когда FETCH_DATA_CLICKED происходит, мы переходим к a FetchingData состояние. Только при входе в это состояние (onEntry) делает FETCH_DATA_REQUEST действие направляется:

{  initial: 'Init',  states: {    Init: {      on: {        FETCH_DATA_CLICKED: 'FetchingData',      },      initial: 'NoData',      states: {        ShowData: {},        Error: {},        NoData: {}      }    },    FetchingData: {      on: {        FETCH_DATA_SUCCESS: 'Init.ShowData',        FETCH_DATA_FAILURE: 'Init.Error',        CLICKED_CANCEL: 'Init.NoData',      },      onEntry: 'FETCH_DATA_REQUEST',      onExit: 'FETCH_DATA_CANCEL',    },  }}

Затем мы меняем нашу эпопею, чтобы реагировать на недавно добавленные FETCH_DATA_REQUEST действие вместо этого:

function handleFetchDataRequest(action$, store) {  // handling FETCH_DATA_REQUEST rather than FETCH_DATA_CLICKED  return action$.ofType('FETCH_DATA_REQUEST')    .mergeMap(action =>      ajax('        .mapTo({ type: 'FETCH_DATA_SUCCESS' })        .takeUntil(action$.ofType('FETCH_DATA_CANCEL'))    )}

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

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

VQAnmHywghmtUrY0I241qgxsOPqyowmMesY5

Интересным свойством этого конкретного дизайна является то, что когда мы выходим из FetchingData состояние, в FETCH_DATA_CANCEL действие отправляется. Мы можем отправлять действия не только при входе в станы, но и при выходе из них. Как определено в нашей эре, это приведет к прерыванию запроса HTTP.

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

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

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

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

tTa5yNmBDRIpuTDoyuFprSRWvA1ZnJUngIH8

Проблемы, помогающие решить таблицы состояний

Как разработчики интерфейса, наша работа состоит в том, чтобы оживить статические изображения. Этот процесс имеет несколько проблем:

  • Когда мы превращаем статические изображения в код, мы теряем понимание высокого уровня нашей программы по мере того, как наша программа развивается, понять, какой раздел кода отвечает за каждое изображение, становится все сложнее.
  • Не на все вопросы можно ответить с помощью набора изображений — Что происходит, когда пользователь нажимает несколько раз? Что делать, если пользователь хочет отменить запрос во время полета?
  • События разбросаны по нашему коду и имеют непредсказуемые последствия — Что происходит, когда пользователь нажимает кнопку? Нам нужна лучшая абстракция, которая поможет нам понять последствия стрельбы.
  • Много isFetching, isShowing, isDisabled переменные — Нам нужно следить за всеми изменениями в нашем интерфейсе.

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

6WmId-UGRupVjl2O5T923hsdkBIvf7Af2KZj

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

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

Учите больше

В качестве конкретного примера я создал redux-statecharts, промежуточное программное обеспечение Redux, которое можно использовать, как показано в предыдущих примерах. Он использует библиотеку xstate – чистую функцию для перехода диаграммы состояний.

Если вы хотите узнать больше о диаграммах состояний, вот отличный ресурс: https://statecharts.github.io/

Также просмотрите мою презентацию по теме: есть ли диаграммы состояний следующей большой парадигмой пользовательского интерфейса?

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *