JS: Архитектура фронтенда

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

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

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

const state = {
  registrationProcess: {
    valid: true,
    submitDisabled: true,
    isLoading: 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 (библиотека для моделирования конечных автоматов)

<span class="translation_missing" title="translation missing: ru.web.courses.lessons.mentors.mentor_avatars">Mentor Avatars</span>

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты.

Ошибки, сложный материал, вопросы >
Нашли опечатку или неточность?

Выделите текст, нажмите ctrl + enter и отправьте его нам. В течение нескольких дней мы исправим ошибку или улучшим формулировку.

Что-то не получается или материал кажется сложным?

Загляните в раздел «Обсуждение»:

  • задайте вопрос. Вы быстрее справитесь с трудностями и прокачаете навык постановки правильных вопросов, что пригодится и в учёбе, и в работе программистом;
  • расскажите о своих впечатлениях. Если курс слишком сложный, подробный отзыв поможет нам сделать его лучше;
  • изучите вопросы других учеников и ответы на них. Это база знаний, которой можно и нужно пользоваться.
Об обучении на Хекслете

Для полного доступа к курсу нужна профессиональная подписка

Профессиональная подписка откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
115
курсов
892
упражнения
2241
час теории
3196
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 115 курсов, 2000+ часов теории
  • 800 практических заданий в браузере
  • 250 000 студентов

Отправляя форму, вы соглашаетесь c «Политикой конфиденциальности» и «Условиями оказания услуг».

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Rambler
Логотип компании Bookmate
Логотип компании Botmother

Есть вопрос или хотите участвовать в обсуждении?

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы соглашаетесь c «Политикой конфиденциальности» и «Условиями оказания услуг».