State Machine: когда State уже недостаточно

State Machine: когда State уже недостаточно

Паттерн State отлично подходит для управления поведением объекта в зависимости от его состояния. Но что делать, когда логика переходов между состояниями усложняется?

Например в наших требованиях появляются:

  • Множественные события перехода в одно состояние.
  • Сложные условия перехода, зависящие от внешних факторов.
  • Асинхронные переходы - например, ожидание ответа от API.
  • Необходимость мониторинга и/или отката состояний (нужна история состояний).
  • Динамическая конфигурация состояний - логика состояний должна быть гибкой и изменяемой без перекомпиляции кода.

В таких случаях подходящим решением будет использование - State Machine/Finite State Mashine (FSM - Конечный автомат).

🏛 История появления FSM

Идея конечного автомата (FSM) уходит корнями в математику и теорию автоматов 1940–1950-х годов. Она была описана в работах таких учёных, как Алан Тьюринг, Стивен Клини и Майкл Рабин, как модель вычислений.

В 1956 году — Джордж Мили и Эдвард Мур (не путать с Гордоном Муром из Intel) независимо друг от друга формализовали конечные автоматы (Finite State Machines, FSM):

  • Автомат Мили — выход зависит от состояния и входа.
  • Автомат Мура — выход зависит только от состояния. Эти модели стали основой для проектирования цифровых схем, компиляторов, сетевых протоколов и теории формальных языков.

FSM стали основой цифровой логики, компиляторов, сетевых протоколов и теории формальных языков. Стоит отметить: В книге "Design Patterns: Elements of Reusable Object-Oriented Software" (банда четырёх, GoF) описан паттерн State, но не State Machine/FSM.

⚙ Преимущества и недостатки State Machine

✅ Преимущества:

  • Централизованная логика и наглядная модель переходов между состояниями.
  • Легко визуализировать (например, с помощью диаграмм состояний). Некоторые реализации FSM позволяют генерировать диаграммы по коду.
  • Удобно использовать для отладки и логирования.
  • Поддерживаются сложные переходы, события, действия и условные переходы.
  • Хорошо масштабируется.
  • Легко добавлять новые состояния и переходы, не затрагивая существующую логику.
  • Много готовых реализаций.

❌ Недостатки:

  • Требует больше кода и архитектурной подготовки.
  • Может быть избыточна для простых сценариев.
  • Реализация может быть очень громоздкой.
  • Если машина состояний слишком большая, её сложно читать и отлаживать.

💡 Интересная реализация State Machine - Табличная (Table-Driven) реализация

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

Сначала определим основы:

enum State { IDLE, PROCESSING, COMPLETED, ERROR } enum Event { START, SUCCESS, FAIL, RESET } interface StateHandler { void onEnter(State from, State to, Event event); }

Теперь определим State Machine с таблицей переходов:

public class TableBasedStateMachine { private State currentState = State.IDLE; // Таблица переходов: event -> (от какого состояния -> в какое состояние) private static final Map<Event, Map<State, State>> transitions = Map.of( Event.START, Map.of(State.IDLE, State.PROCESSING), Event.SUCCESS, Map.of(State.PROCESSING, State.COMPLETED), Event.FAIL, Map.of(State.PROCESSING, State.ERROR), Event.RESET, Map.of(State.COMPLETED, State.IDLE, State.ERROR, State.IDLE)); // Обработчики входа в состояние private static final Map<State, StateHandler> handlers = Map.of( State.IDLE, (from, to, event) -> System.out.println("Entered IDLE from " + from + " via " + event), State.PROCESSING, (from, to, event) -> System.out.println("Started processing..."), State.COMPLETED, (from, to, event) -> System.out.println("Processing completed successfully."), State.ERROR, (from, to, event) -> System.out.println("Error occurred during processing.")); public void handle(Event event) { Map<State, State> stateMap = transitions.getOrDefault(event, Map.of()); State nextState = stateMap.get(currentState); if (nextState != null && nextState != currentState) { State oldState = currentState; currentState = nextState; // Вызов обработчика входа в новое состояние StateHandler handler = handlers.getOrDefault(currentState, (f, t, e) -> { }); handler.onEnter(oldState, currentState, event); System.out.println("Transition: " + oldState + " -> " + currentState); } else { System.out.println("No transition from " + currentState + " on " + event); } } public State getCurrentState() { return currentState; } }

🕹 Тестирование и запуск:

public class Main { public static void main(String[] args) { TableBasedStateMachine sm = new TableBasedStateMachine(); sm.handle(Event.START); // IDLE -> PROCESSING sm.handle(Event.SUCCESS); // PROCESSING -> COMPLETED sm.handle(Event.RESET); // COMPLETED -> IDLE sm.handle(Event.FAIL); // No transition } }

💎 Преимущества такого подхода:

  • Наглядная таблица переходов — легко читать, расширить или загрузить из конфигурационных файлов (YAML/JSON).
  • Изоляция разработчиков — легко тестируются и не захламляют переходную логику.В отличие от классического switch, добавление нового состояния — просто расширение таблицы.
  • Возможность использовать одну и ту же таблицу переходов, но разные обработчики.

📌 Некоторые выводы

Паттерн State Machine — это подходящий инструмент для моделирования систем, поведение которых сильно зависит от их состояний и логики переходов между ними. Он становится незаменимым, когда State уже не справляется с растущей сложностью. Когда логика переходов и состояний усложняется, стоит рассмотреть готовые реализации, которые предлагают множество дополнительных полезных обвязок - сохранение состояния в БД, визуализация диаграмм состояний и т.п. Одну из таких реализаций - spring state machine мы рассмотрим в следующих постах.

Кто реализовывал State Machine самостоятельно?

Канал автора в telegram

1
Начать дискуссию