Процессы и автоматы, их описывающие

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

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

const state = {
  registrationProcess: {
    valid: true,
    submitDisabled: true,
  }
};

В реальных приложениях всё ещё сложнее. Во время отправки данных блокируется не только кнопка ввода, но и поле для ввода. Более того, отправка данных в одном месте, может повлиять и на остальные блоки на странице, которые могут пропадать, блокироваться или видоизменяться. Не говоря уже о том, что причин блокировки кнопки может быть несколько. Она может быть заблокирована просто потому, что в форму введены некорректные данные. В таком случае уже недостаточно простых true или false.

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

const state = {
  registrationProcess: {
    valid: true,
    submitDisabled: true,
    inputDisabled: true,
    showSpinner: true,
    blockAuthentication: true,
  }
};

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

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

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

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

  • filling – заполнение формы. В этом состоянии всё активно и доступно для редактирования.
  • processing (или sending) – отправка формы. Это то самое состояние, когда пользователь ждёт, а приложение пытается предотвратить нежелательные действия, например, клики или изменения данных формы.
  • processed (или finished) – состояние, обозначающее что всё завершилось. В нём форма уже не отображается.
  • failed – состояние, обозначающее завершение с ошибкой. Например произошел сбой в сети во время загрузки или загруженные данные оказались неверными.

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

const state = {
  registrationProcess: {
    state: 'filling',
  }
};

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

// Этих "ифов" может быть сколько угодно,
// главное, что они завязаны на общее состояние, а не проверку конкретных флагов
if (state.registrationProcess.state === 'processing') {
  // Блокируем кнопки
  // Активируем спиннеры
}

if (state.registrationProcess.state === 'failed') {
  // Выводим сообщение об ошибке
}

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

const state = {
  registrationProcess: {
    errors: ['Имя не заполнено', 'Адрес имеет неверный формат'],
    state: 'processed',
  }
};

Причём этот же массив с ошибками удобно использовать для валидации формы до отправки на сервер. То есть будучи в состоянии filling.

А что если мы захотим блокировать возможность отправки формы до того момента, пока не пройдёт валидация на фронтенде? Есть два подхода: либо мы проверяем, что errors пуст, либо, что лучше, мы вводим явное состояние валидности формы. И тогда состояние нашего приложения становится таким:

const state = {
  registrationProcess: {
    errors: ['Имя не заполнено', 'Адрес имеет неверный формат'],
    state: 'processed',
    validationState: 'invalid' // или valid
  }
};

В некоторых ситуациях возможно объединение, когда процесс валидации соединён с процессом обработки самой регистрации. Тогда вместо отдельного состояния validationState, появится дополнительное состояние invalid внутри state. Это не совсем корректно с точки зрения моделирования (потому что у нас действительно два разных процесса), но иногда такой способ позволяет написать чуть более простой код (до тех пор пока различий не станет много).

Глобально, такой подход в разработке, называется программированием с явным выделенным состоянием. Он сводится к тому, что в рамках приложения находятся базовые процессы, от которых зависит всё остальное. Затем эти процессы моделируются с помощью конечных автоматов (FSM). Причём не важно, какие инструменты используются для разработки: чистый DOM, jQuery или любой мощный современный фреймворк. Он применим везде и везде нужен.

Это невероятно мощная парадигма программирования, которая описана в книге "Автоматное Программирование" в наших рекомендациях.


Дополнительные материалы

  1. Xstate (библиотека для моделирования конечных автоматов)
Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Javascript, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →