В мире разработки программного обеспечения управление состоянием объекта - одна из фундаментальных задач. Когда поведение объекта должно меняться в зависимости от его внутреннего состояния, разработчики часто обращаются к паттерну State. Однако здесь и возникает путаница: его нередко отождествляют с более общей концепцией — State Machine (Конечный автомат), а то и вовсе не видят разницы.

На самом деле, State — это лишь один из способов реализовать конечный автомат в ООП, и он прекрасен в своей простоте. Но что делать, когда логика усложняется, появляются асинхронные переходы, а количество состояний и событий растет? В этот момент простой паттерн State уступает место полноценным State Machine (FSM). А когда и их возможностей становится недостаточно для описания по-настоящему сложных систем с иерархией и параллельными процессами, на сцену выходят Statecharts.

В этой статье пройдем путь от простого к сложному:

  • Разберем паттерн State как самостоятельную единицу и поймем, где его применять.

  • Выясним, в какой момент его возможностей становится мало и на помощь приходит State Machine, рассмотрев практичную табличную реализацию.

  • Наконец, доберемся до Statecharts и их реализации в Spring State Machine, которая позволяет элегантно управлять даже самой запутанной логикой состояний.

Погрузимся в мир управления состояниями — от простого к сложному!

Статья будет полезна Java-разработчикам, архитекторам и всем, кто хочет глубже понять поведенческие паттерны и их практическое применение.

State ≠ State Machine: разбираем поведенческий паттерн, который часто путают

State паттерн ("Состояние") — это поведенческий паттерн проектирования, который позволяет объекту изменять своё поведение в зависимости от внутреннего состояния. Многие описывают этот паттерн в контексте паттерна State Machine ("Конечный автомат", "Машина состояний") и не рассматривают в отрыве от этой концепции. На самом деле State самостоятельный паттерн и часто используется независимо.

Когда использовать State

Паттерн State особенно полезен когда:

  • Логика поведения объекта зависит от его различных состояний.

  • Логика содержит множество условных операторов (if/else, switch), зависящих от текущего состояния или внутренних флагов.

  • Хотим убрать связь между состоянием и поведением.

  • Есть множество состояний и они могут меняться со временем.

Примеры:

  • Текстовый редактор с режимами: чтение, редактирование, навигация.

  • Состояния заказа/платежа.

  • Жизненный цикл кредита/займа.

  • Транзакции между счетами.

  • Активность пользовательского аккаунта.

Пример реализации на Java

Рассмотрим реализацию состояний текстового редактора.

Есть множество вариантов реализации патт��рна State, некоторые из них могут нарушать принцип LSP(Liskov Substitution Principle), но тем не менее они представлены на множестве ресурсов. Рассмотрим вариант без нарушения принципа LSP:

Определяем интерфейс состояния:

public interface EditorState {
    void handle(TextEditor context);
}

Конкретные состояния реализуют интерфейс EditorState:

public class ReadingState implements EditorState {
    @Override
    public void handle(TextEditor context) {
        System.out.println("Вы находитесь в режиме чтения. Просматривайте документ.");
        System.out.println("Переход в режим редактирования.");
        context.setState(new EditingState());
    }
}

public class EditingState implements EditorState {
    @Override
    public void handle(TextEditor context) {
        System.out.println("Вы находитесь в режиме редактирования. Вносите изменения в текст.");
        System.out.println("Переход в режим навигации.");
        context.setState(new NavigatingState());
    }
}

public class NavigatingState implements EditorState {
    @Override
    public void handle(TextEditor context) {
        System.out.println("Вы находитесь в режиме навигации. Просматривайте разделы.");
        System.out.println("Переход в режим чтения.");
        context.setState(new ReadingState());
    }

}

Контекст, который хранит текущее состояние:

public class TextEditor {
    // Текущее состояние редактора
    private EditorState currentState;

    public TextEditor() {
        this.currentState = new EditingState(); // начальное состояние
    }

    public void setState(EditorState state) {
        this.currentState = state;
    }

    public void performAction() {
        currentState.handle(this);
    }
}

Тестирование:

public class Main
    public static void main(String[] args) {
        TextEditor editor = new TextEditor();

        // Цикл переключений состояний
        for (int i = 0; i < 3; i++) {
            editor.performAction();
            System.out.println("----------------------------");
        }
    }
}

Результат:

Вы находитесь в режиме редактирования. Вносите изменения в текст.
Переход в режим навигации.
----------------------------
Вы находитесь в режиме навигации. Просматривайте разделы.
Переход в режим чтения.
----------------------------
Вы находитесь в режиме чтения. Просматривайте документ.
Переход в режим редактирования.
----------------------------

Преимущества, которые мы получим, когда используем State

  • Более чистый и структурированный код за счет применения Single Responsibility и Open-Closed принципов ООП.

  • Уменьшается связанность между компонентами.

  • Расширяемость - добавление нового состояния требует минимального изменения существующих.

  • Инкапсуляция логики.

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, добавление нового состояния — просто расширение таблицы.

  • Возможность использовать одну и ту же таблицу переходов, но разные обработчики.

Statechart и Spring State Machine: когда FSM уже недостаточно

Выше мы рассмотрели классические State Machine. Посмотрели на реализацию FSM с использованием таблиц. Но что делать, когда система разрастается и появляются такие требования:

  • Вложенные (иерархические) состояния: состояние может иметь подсостояния, и переход из родительского состояния влияет на все его дочерние состояния.

  • Параллельные (ортогональные) состояния: объект может находиться в нескольких независимых состояниях одновременно.

  • История состояний: Возможность вернуться в последнее активное подсостояние после выхода из родительского состояния.

  • Входные/выходные действия: действия, которые выполяются при входе или выходе из состояния.

  • Визуализация сложной логики переходов: логика переходов становится настолько большой, что ее трудно понять, посмотрев код и требуется некоторая визуализация.

Здесь на помощь приходит Statechart — расширение FSM, предложенное Дэвидом Харелом в 1987 году. Он разработал их для моделирования сложных авиационных систем, где плоские FSM приводили к "взрыву состояний" и становились неуправляемыми. Ссылка на статью по Statechart (здесь немного модифицированная статья) - https://www.inf.ed.ac.uk/teaching/courses/seoc/2005_2006/resources/statecharts.pdf. Здесь более подробно и с математическими выводами - https://is.ifmo.ru/works/pattern.pdf

Statechart: Эволюция конечных автоматов

Основная идея Statechart - добавление концепции иерархии (вложенных состояний), параллелизма и истории, что позволяет создавать более компактные и наглядные модели сложных систем.

Ключевые особенности Statechart:

  • Вложенные состояния (Nested States): состояние может содержать другие состояния. Это позволяет группировать связанные состояния и упрощает диаграммы, так как переходы из родительского состояния применяются ко всем его дочерним состояниям.

  • Параллельные состояния (Concurrent/Orthogonal States): состояние может быть разделено на несколько независимых ортогональных областей, каждая из которых имеет свой собственный конечный автомат. Это моделирует ситуации, когда объект одновременно выполняет несколько независимых функций.

  • История (History Pseudo-States): позволяет автомату запоминать последнее активное подсостояние в составном состоянии, чтобы при повторном входе в него вернуться именно в это подсостояние.

  • Entry/Exit Actions: действия, которые выполняются при входе в состояние или выходе из него.

  • Internal Transitions: переходы, которые обрабатываются внутри состояния без выхода из него.

Statechart является частью UML (Unified Modeling Language), что делает его широко распространенным и понятным инструментом для моделирования поведения систем. Statechart идеально подходит для систем с множеством взаимосвязанных состояний, например, в workflow-движках или IoT-устройствах.

Spring State Machine: реализация Statechart на Java

Spring State Machine (SSM) — это фреймворк, предоставляющий реализацию концепции Statechart для Spring-приложений. Он предлагает декларативный подход к определению состояний и переходов, множество возможностей для гибкой настройки, интеграцию с другими Spring-проектами и многое другое.

Основные возможности Spring State Machine:

  • Декларативное определение: состояния и переходы определяются с помощью конфигурации (Java-конфигурации или XML).

  • Вложенные состояния: полная поддержка иерархических состояний.

  • Параллельные состояния: возможность моделировать параллельные процессы.

  • Действия (Actions): можно привязывать действия к переходам, входу/выходу из состояний.

  • Условные переходы (Guards): условия, которые должны быть выполнены для осуществления перехода.

  • События (Events): механизм для запуска переходов.

  • Персистентность (Persistence): возможность сохранять и восстанавливать состояние конечного автомата (например, в БД).

  • Мониторинг: интеграция с Spring Actuator для мониторинга состояния машины.

  • Визуализация: можно определять состояния и переходы используя диаграммы состояний через eclipse Papyrus framework (к сожалению в 4 версии эту возможность удалили), вот тут описано как это сделать - https://docs.spring.io/spring-statemachine/docs/current/reference/#sm-papyrus.

Давайте рассмотрим пример использования Spring State Machine для сценария "обработки запроса", с более сложной логикой, включая вложенные состояния.

Обработка Запроса с Вложенными Состояниями

Пусть, наше состояние PROCESSING может быть детализировано на подсостояния: INITIALIZING, EXECUTING, FINALIZING. -

/ Перечисления для состояний и событий
enum States {
    IDLE,
    PROCESSING,
    PROCESSING_INITIALIZING,
    PROCESSING_EXECUTING,
    PROCESSING_FINALIZING,
    COMPLETED,
    ERROR
}

enum Events {
    START,
    INITIALIZE_DONE,
    EXECUTE_DONE,
    FINALIZE_DONE,
    SUCCESS,
    FAIL,
    RESET
}

Сама конфигурация StateMachine с логикой переходов между состояниями, условиями и вложенными состояниями будет выглядеть так:

@Configuration
@EnableStateMachine
public class RequestStateMachineConfig extends StateMachineConfigurerAdapter<States, Events> {

    @Override
    public void configure(StateMachineConfigurationConfigurer<States, Events> config) throws Exception {
        config
                .withConfiguration()
                .autoStartup(true)
                .listener(new StateMachineListener()); // Добавим слушателя для логирования
    }

    @Override
    public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception {
        states
                .withStates()
                .initial(States.IDLE)
                .state(States.COMPLETED)
                .state(States.ERROR)
                .state(States.PROCESSING, processingActions()) // Действия для родительского состояния PROCESSING
                .end(States.COMPLETED) // Конечные состояния для ветки
                .end(States.ERROR)
                .and()
                .withStates()
                .parent(States.PROCESSING) // Определяем вложенные состояния
                .initial(States.PROCESSING_INITIALIZING)
                .state(States.PROCESSING_EXECUTING)
                .state(States.PROCESSING_FINALIZING);
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception {
        transitions
                .withExternal()
                .source(States.IDLE).target(States.PROCESSING).event(Events.START)
                .action(startProcessingAction())
                .and()
                .withExternal()
                .source(States.PROCESSING_INITIALIZING).target(States.PROCESSING_EXECUTING)
                .event(Events.INITIALIZE_DONE)
                .action(initializeDoneAction())
                .and()
                .withExternal()
                .source(States.PROCESSING_EXECUTING).target(States.PROCESSING_FINALIZING).event(Events.EXECUTE_DONE)
                .action(executeDoneAction())
                .and()
                .withExternal()
                .source(States.PROCESSING_FINALIZING).target(States.COMPLETED).event(Events.FINALIZE_DONE)
                .action(finalizeDoneAction())
                .and()
                .withExternal()
                .source(States.PROCESSING).target(States.ERROR).event(Events.FAIL) // Переход из родительского состояния
                .action(failAction())
                .and()
                .withExternal()
                .source(States.COMPLETED).target(States.IDLE).event(Events.RESET)
                .action(resetAction())
                .and()
                .withExternal()
                .source(States.ERROR).target(States.IDLE).event(Events.RESET)
                .action(resetAction());
    }

    // Действия (Actions)
    public Action<States, Events> startProcessingAction() {
        return context -> System.out.println("Entering PROCESSING state from IDLE.");
    }

    public Action<States, Events> initializeDoneAction() {
        return context -> System.out.println("Processing: Initializing done. Moving to EXECUTING.");
    }
    // ....

    // Логирование состояний
    static class LoggingStateStateMachineListener
            extends StateMachineListenerAdapter<States, Events> {
        @Override
        public void stateChanged(State<States, Events> from, State<States, Events> to) {
            if (from != null) {
                System.out.println("Transition: " + from.getId() + " -> " + to.getId());
            } else {
                System.out.println("Initial state: " + to.getId());
            }
        }
    }
}

Проверить результаты работы можно через запуск:

@SpringBootApplication
public class SpringFsmDemoApplication implements CommandLineRunner {

	@Autowired
    private StateMachine<States, Events> stateMachine;

	public static void main(String[] args) {
		SpringApplication.run(SpringFsmDemoApplication.class, args);
	}

	@Override
	public void run(String... args) throws Exception {
		System.out.println("Current state: " + stateMachine.getState().getId());

        stateMachine.sendEvent(Events.START);
        System.out.println("Current state: " + stateMachine.getState().getId()); // Должно быть PROCESSING_INITIALIZING

        stateMachine.sendEvent(Events.INITIALIZE_DONE);
        System.out.println("Current state: " + stateMachine.getState().getId()); // Должно быть PROCESSING_EXECUTING

        stateMachine.sendEvent(Events.EXECUTE_DONE);
        System.out.println("Current state: " + stateMachine.getState().getId()); // Должно быть PROCESSING_FINALIZING

        stateMachine.sendEvent(Events.FINALIZE_DONE);
        System.out.println("Current state: " + stateMachine.getState().getId()); // Должно быть COMPLETED

        stateMachine.sendEvent(Events.RESET);
        System.out.println("Current state: " + stateMachine.getState().getId()); // Должно быть IDLE

	}
}

Полный код проекта доступен по ссылке.

Итоги

Выбор инструмента для управления состояниями напрямую зависит от масштаба задачи и сложности.

  1. Паттерн State — отличный выбор, когда нужно инкапсулировать логику, зависящую от состояния, внутри объекта, избавляясь от громоздких условных операторов. Он прост, элегантен и идеально подходит для сценариев, где переходы между состояниями несложны и управляются самим объектом. Это отличная реализация принципов SOLID для локальных задач.

  2. State Machine (FSM) — это следующий шаг, когда логика переходов становится явной и централизованной. FSM необходима, когда есть четко определенные события (events), вызывающие переходы.

  3. Statecharts (и их реализация, например, Spring State Machine) — это решение для промышленных систем. Стоит использовать когда нужны:

    • Иерархические (вложенные) состояния для группировки логики.

    • Параллельные (ортогональные) состояния для моделирования независимых процессов.

    • История состояний, условные переходы (guards) и действия (actions).

Таблица с сравнением характеристик:

Характеристика

State

State Machine (FSM)

Statechart / Spring SM

Простота

✅ Простая

⚠️ Средняя сложность

❗ Сложная

Поддержка событий

❌ Нет

✅ Да

✅ Да

Вложенные состояния

❌ Нет

❌ Нет

✅ Да

Поддержка истории

❌ Нет

❌ Нет

✅ Да

Параллельные состояния

❌ Нет

❌ Нет

✅ Да

Визуализация

❌ Ограничена

✅ Возможна

✅ Встроенная поддержка

Spring State Machine предоставляет весь этот арсенал "из коробки", интегрируя его в экосистему Spring и добавляя персистентность, мониторинг и возможность визуализации (не во всех версиях).

Проще говоря, начинайте с State. Если переходы становятся сложными и требуют явного управления — переходите к State Machine. А если ваша система напоминает сложный производственный процесс - смело берите на вооружение Statecharts. Правильный выбор не только сделает ваш код чище, но и сэкономит массу времени на отладку и поддержку в будущем.

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

Спасибо за внимание!

Трансформируйтесь от хаоса к гармонии.

Если интересно, подписывайтесь на мой канал, там больше тем, которые не входят в формат Хабра.