Конечные автоматы (FSM)
Конечные автоматы (Finite State Machines, FSM) — это математическая модель поведения системы, которая в каждый момент времени находится в одном из конечного набора состояний.
Примеры reducer-ов на этой странице написаны в стиле Immer: они мутируют draft
stateи предполагают, чтоMachineManagerсоздан сimmerMiddleware. Без middleware reducer должен вернуть новый объект{ state, context }.
Основные компоненты конечного автомата
- Состояния — конечный набор возможных состояний системы.
- События — действия, которые могут вызвать переход из одного состояния в другое.
- Переходы — функции, определяющие новое состояние на основе текущего состояния и события.
- Начальное состояние — состояние, в котором система находится до начала работы.
Визуализация конечного автомата
Конечные автоматы часто представляют в виде диаграмм состояний, где:
- Состояния изображаются как круги или прямоугольники
- Переходы изображаются как стрелки между состояниями
- Стрелки помечаются событиями, которые вызывают переход
Пример диаграммы минимального автомата:
+-------+ TURN_ON +-------+
| |------------->| |
| OFF | | ON |
| |<-------------| |
+-------+ TURN_OFF +-------+Преимущества использования конечных автоматов
1. Предсказуемость и строгая структура
FSM делает поведение системы предсказуемым и ясным. В каждый момент времени система находится в определенном состоянии, и переходы между состояниями чётко определены.
2. Управление сложностью
FSM позволяет разделить сложную логику на явные дискретные состояния и правила перехода между ними.
3. Отладка и тестирование
Модель FSM упрощает отладку и тестирование, потому что:
- Состояния можно перечислить и проверить
- Переходы предсказуемы и детерминированы
- Легко понять, как система перешла в текущее состояние
4. Документирование поведения
Диаграммы состояний FSM могут служить наглядной документацией того, как должна работать система.
Применение конечных автоматов
Конечные автоматы широко используются в различных областях:
- UI-компоненты: для управления состояниями интерфейса (открыт/закрыт, активен/неактивен)
- Бизнес-процессы: для моделирования рабочих процессов (новый, в работе, проверка, завершено)
- Протоколы связи: для управления соединениями (подключение, подключено, отключение)
- Игровые механики: для управления состояниями игровых объектов (бездействие, перемещение, атака)
- Парсеры и компиляторы: для разбора и анализа текста
Основные модели реакции на события
При проектировании конечных автоматов важно определить, как система будет реагировать на события. В теории автоматов выделяют две фундаментальные модели:
Автомат Мура
Выходные значения связаны с состояниями, а не с переходами. В концепции statecharts эта модель используется для действий, которые должны выполняться при входе в состояние или пока система находится в определенном состоянии. В lite-fsm эта концепция реализуется через эффекты, привязанные к состояниям:
const lamp = createMachine({
config: {
OFF: {
TURN_ON: "ON",
},
ON: {
TURN_OFF: "OFF",
},
},
initialState: "OFF",
initialContext: { brightness: 0 },
effects: {
// Эффект, привязанный к состоянию ON (автомат Мура)
ON: ({ light }) => light.turnOn(),
},
});Автомат Мили
Выходные значения связаны с переходами, а не с состояниями. В statecharts это отражается в действиях, которые выполняются во время перехода между состояниями. Такой подход удобен, когда важно реагировать именно на изменение состояния или когда одинаковые события должны вызывать разные действия в зависимости от текущего состояния. В lite-fsm это реализуется через reducer, обрабатывающий конкретные события:
const counter = createMachine({
config: {
IDLE: {
INCREMENT: "COUNTING",
},
COUNTING: {
INCREMENT: null,
},
},
initialState: "IDLE",
initialContext: { count: 0 },
reducer: (state, action, { nextState }) => {
state.state = nextState;
if (action.type === "INCREMENT") {
state.context.count += 1;
}
},
});Ограничения базовых конечных автоматов
Плоские конечные автоматы хорошо подходят для моделирования небольших процессов, но при росте системы у них появляются ограничения:
Проблема взрыва состояний
У плоских FSM есть известная проблема — «взрыв состояний». С ростом числа независимых признаков количество возможных комбинаций растёт экспоненциально. Например, 5 независимых булевых флагов дают 2^5 = 32 комбинации, а 10 флагов — уже 1024.
Нарастающая сложность переходов
В крупных приложениях быстро растет не только количество состояний, но и число возможных переходов между ними. Определение и поддержка всех этих переходов становится дорогой и ошибкоопасной задачей.
Отсутствие повторного использования логики
В классических FSM сложно выделить повторяющиеся паттерны и переиспользовать их в различных частях приложения, что приводит к дублированию кода и увеличению шансов на ошибки.
Statecharts (Расширенные диаграммы состояний)
Расширенная модель statecharts (диаграммы состояний), предложенная Дэвидом Харелом, решает проблемы простых FSM, добавляя несколько ключевых концепций:
Примечание: Оригинальная статья Дэвида Харела “Statecharts: A Visual Formalism for Complex Systems” (1987) является фундаментальной работой, определившей концепцию statecharts. Для ознакомления также доступна свободная версия статьи в PDF .
Ключевые концепции Statecharts
1. Иерархия и вложенные состояния
Вложенные (иерархические) состояния группируют связанные состояния внутри родительских. Они используются для:
- Уменьшения сложности: вместо линейного списка из сотен состояний появляется иерархическая структура, которую проще читать
- Наследования поведения: Дочерние состояния могут наследовать обработку событий от родительских состояний
- Модульности: Логика разных функциональных модулей может быть изолирована и разработана независимо
2. Параллельные состояния
Параллельные состояния позволяют моделировать независимые процессы, которые происходят одновременно:
- Независимые аспекты: Форма может одновременно находиться в состояниях “валидная” и “отправляется”
- Декомпозиция сложных систем: Разные части UI (панели, диалоги) могут иметь собственные жизненные циклы
- Предотвращение комбинаторного взрыва: Вместо создания отдельных состояний для каждой комбинации, можно моделировать независимые процессы отдельно
3. Контекст и данные
Расширенная модель включает хранение данных (контекста), что позволяет:
- Уменьшить количество состояний: Вместо создания отдельных состояний для каждого значения данных, можно хранить их в контексте
- Сохранять историю: Можно отслеживать предыдущие действия и принимать решения на их основе
- Принимать условные решения: Переходы могут зависеть от данных в контексте
4. Истории и самопереходы
- Истории позволяют вернуться к предыдущему активному состоянию после выхода и повторного входа
- Самопереходы дают возможность обрабатывать события без изменения состояния, но с обновлением контекста
Практические преимущества Statecharts
Применение statecharts и связанных концепций в крупных приложениях даёт практические преимущества:
- Масштабируемость: возможность моделировать сложные системы без экспоненциального роста состояний
- Понятные границы ответственности: Четкое разделение между различными компонентами и их состояниями
- Устранение невозможных состояний: Структурные ограничения предотвращают попадание системы в “невозможные” состояния
- Прогнозируемое поведение: Легче отследить, как система перешла в определенное состояние
- Упрощение тестирования: Возможность тестировать отдельные компоненты системы независимо
- Документирование системы: Диаграмма statechart сама по себе является наглядной документацией поведения системы
Создание базового автомата
import { createMachine, MachineManager } from "@lite-fsm/core";
const player = createMachine({
config: {
IDLE: {
WALK: "WALKING",
RUN: "RUNNING",
},
WALKING: {
STOP: "IDLE",
SPEED_UP: "RUNNING",
},
RUNNING: {
STOP: "IDLE",
SLOW_DOWN: "WALKING",
},
},
initialState: "IDLE",
initialContext: {
speed: 0,
direction: null as null | "north" | "south" | "east" | "west",
},
});
const manager = MachineManager({ player });
manager.transition({ type: "WALK", payload: { direction: "north", speed: 3 } });Расширенные возможности и паттерны
Иерархические состояния через композицию
В lite-fsm иерархические состояния моделируются с помощью композиции автоматов:
// Вложенный автомат для режимов редактирования
const editor = createMachine({
config: {
TEXT: {
SWITCH_MODE: "VISUAL",
SAVE: null,
},
VISUAL: {
SWITCH_MODE: "TEXT",
SAVE: null,
},
},
initialState: "TEXT",
initialContext: { content: "" },
});
// Основной автомат, который управляет текущим режимом работы
const document = createMachine({
config: {
VIEWING: {
EDIT: "EDITING",
},
EDITING: {
SAVE: "VIEWING",
CANCEL: "VIEWING",
},
},
initialState: "VIEWING",
initialContext: {},
});
const manager = MachineManager({ document, editor });Связанные состояния через композицию
Пример двух машин, которые реагируют на одно событие и вместе описывают процесс:
const uploadFlow = createMachine({
config: {
WAITING: {
START_UPLOAD: "UPLOADING",
},
UPLOADING: {
UPLOAD_DONE: "COMPLETE",
},
COMPLETE: {},
},
initialState: "WAITING",
initialContext: {},
});
const uploadProgress = createMachine({
config: {
INACTIVE: {
START_UPLOAD: "TRACKING",
},
TRACKING: {
PROGRESS: null,
UPLOAD_DONE: "INACTIVE",
},
},
initialState: "INACTIVE",
initialContext: { percent: 0 },
reducer: (state, action, { nextState }) => {
state.state = nextState;
if (action.type === "PROGRESS") state.context.percent = action.payload.percent;
if (action.type === "START_UPLOAD") state.context.percent = 0;
},
});
const manager = MachineManager({ uploadFlow, uploadProgress });
// Одно событие обрабатывают оба автомата
manager.transition({ type: "START_UPLOAD" });Параллельные состояния
Параллельные состояния реализуются через независимые автоматы:
const playback = createMachine({
config: {
PLAYING: {
PAUSE: "PAUSED",
},
PAUSED: {
PLAY: "PLAYING",
},
},
initialState: "PAUSED",
initialContext: {},
});
const audio = createMachine({
config: {
MUTED: {
UNMUTE: "UNMUTED",
},
UNMUTED: {
MUTE: "MUTED",
VOLUME_CHANGE: null,
},
},
initialState: "UNMUTED",
initialContext: { volume: 50 },
});
const manager = MachineManager({ playback, audio });Самопереходы
Самопереходы позволяют обрабатывать события без изменения состояния:
const draft = createMachine({
config: {
EDITING: {
UPDATE_TITLE: null, // Самопереход — остаёмся в EDITING
SUBMIT: "SUBMITTED",
},
SUBMITTED: {},
},
initialState: "EDITING",
initialContext: { title: "" },
reducer: (state, action, { nextState }) => {
state.state = nextState;
if (action.type === "UPDATE_TITLE") {
state.context.title = action.payload.title;
}
},
});Финальные состояния
Финальные состояния моделируются как состояния без исходящих переходов:
const process = createMachine({
config: {
IDLE: {
START: "PROCESSING",
},
PROCESSING: {
COMPLETE: "COMPLETED",
ERROR: "FAILED",
},
COMPLETED: {}, // Финальное состояние — нет исходящих переходов
FAILED: {
RETRY: "PROCESSING",
},
},
initialState: "IDLE",
initialContext: {},
});Условные переходы
Условные переходы реализуются с помощью reducer:
const auth = createMachine({
config: {
CHECKING: {
AUTH_SUCCESS: "AUTHENTICATED",
AUTH_FAILURE: "ERROR",
},
AUTHENTICATED: {
LOGOUT: "LOGGED_OUT",
},
ERROR: {
RETRY: "CHECKING",
},
LOGGED_OUT: {
LOGIN: "CHECKING",
},
},
initialState: "CHECKING",
initialContext: {
user: null as null | { id: string },
isAdmin: false,
},
reducer: (state, action, { nextState }) => {
// Guard: события без admin-роли не меняют состояние и контекст
if (action.type === "AUTH_SUCCESS" && action.payload.role !== "admin") {
return;
}
state.state = nextState;
if (action.type === "AUTH_SUCCESS") {
state.context.user = action.payload.user;
state.context.isAdmin = true;
}
},
});Заключение
Моделирование поведения с помощью конечных автоматов и statecharts делает состояния и переходы приложения явными. Выбор между плоской FSM, композицией автоматов и statecharts-моделью с иерархией и параллельными состояниями зависит от сложности процесса.
lite-fsm не предоставляет прямую поддержку всех возможностей statecharts в одном автомате, например вложенных состояний внутри одной машины. Для таких сценариев библиотека предлагает композицию автоматов, разделение ответственности и систему effects.