Решение проблемы избыточных состояний через конечные автоматы



Часто замечаю в коде у фронтенд-разработчиков такой паттерн, как выражение одного логического состояния через несколько состояний в коде. Рассмотрим, например, такой код на 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. Таким образом, мы получили удобное декларативное описание возможных состояний нашей системы, а также совокупность переходов из одного состояния в другое.