Решение проблемы избыточных состояний через конечные автоматы
Часто замечаю в коде у фронтенд-разработчиков такой паттерн, как выражение одного логического состояния через несколько состояний в коде. Рассмотрим, например, такой код на React, который описывает состояние некоторой формы:
Здесь разработчик создает целых три разных независимых состояний в коде (состояние ошибки, состояние загрузки, состояние успеха), но логически весь этот код отвечает за состояние формы - то есть за одно и то же состояние. Это одна форма, которая может либо находиться в одном из четырех состояний:
1. в состоянии загрузки
2. в состоянии ошибки
3. в состоянии успеха
4. в состоянии готовности к редактированию.
Несложно посчитать, что код выше сгенерирует нам целых восемь различных состояний вместо четырех необходимых. Помимо запутанности и сложночитаемости, такой код плох тем, что мы генерируем недостижимые состояния, которые непонятно, как обрабатывать. Например, что делать, если у нас одновременно есть ошибка и переменная isSuccessful равна true? Скорее всего, программист выберет какое-то одно состояние как приоритетное, но так или иначе, такой код будет порождать запутанность и баги.
Выход из этой ситуации - это использование конечных автоматов. Конечный автомат - это математическая модель, описывающая конечный набор возможных состояний и определяющая, что автомат может находится только в одном из этих состояний в конкретный момент времени. Также автомат может переходить из одного состояния в другое. В коде на React конечный автомат можно довольно создать, используя редюсер, а возможные состояния удобно описываются через тип-сумму:
Созданные formReducer и initialState мы можем использовать в нашем компоненте через useReducer. Таким образом, мы получили удобное декларативное описание возможных состояний нашей системы, а также совокупность переходов из одного состояния в другое.
Часто замечаю в коде у фронтенд-разработчиков такой паттерн, как выражение одного логического состояния через несколько состояний в коде. Рассмотрим, например, такой код на React, который описывает состояние некоторой формы:
const [error, setError] = useState<string | undefined>(); // в форме есть ошибки
const [loading, setLoading] = useState<boolean>(false); // форма загружается на сервер
const [isSuccessful, setIsSuccessful] = useState<boolean>(false); // форма успешно отправлена
Здесь разработчик создает целых три разных независимых состояний в коде (состояние ошибки, состояние загрузки, состояние успеха), но логически весь этот код отвечает за состояние формы - то есть за одно и то же состояние. Это одна форма, которая может либо находиться в одном из четырех состояний:
1. в состоянии загрузки
2. в состоянии ошибки
3. в состоянии успеха
4. в состоянии готовности к редактированию.
Несложно посчитать, что код выше сгенерирует нам целых восемь различных состояний вместо четырех необходимых. Помимо запутанности и сложночитаемости, такой код плох тем, что мы генерируем недостижимые состояния, которые непонятно, как обрабатывать. Например, что делать, если у нас одновременно есть ошибка и переменная isSuccessful равна true? Скорее всего, программист выберет какое-то одно состояние как приоритетное, но так или иначе, такой код будет порождать запутанность и баги.
Выход из этой ситуации - это использование конечных автоматов. Конечный автомат - это математическая модель, описывающая конечный набор возможных состояний и определяющая, что автомат может находится только в одном из этих состояний в конкретный момент времени. Также автомат может переходить из одного состояния в другое. В коде на React конечный автомат можно довольно создать, используя редюсер, а возможные состояния удобно описываются через тип-сумму:
type FormState = {
state: 'loading';
} | {
state: 'error';
message: string;
} | {
state: 'ready';
} | {
state: 'successful';
}
type FormAction = {
type: 'set_loading'
} | {
type: 'set_error',
payload: {
error_message: string;
}
} | {
type: 'set_ready',
} | {
type: 'set_successful'
}
const initialState: FormState = { state: 'ready' };
const formReducer = (state: FormState, action: FormAction):FormState => {
// ...
}
Созданные formReducer и initialState мы можем использовать в нашем компоненте через useReducer. Таким образом, мы получили удобное декларативное описание возможных состояний нашей системы, а также совокупность переходов из одного состояния в другое.