🧩 Раздел 2 · Вопрос #13

В чём разница между State и Strategy

С точки зрения структуры классов паттерны State (Состояние) и Strategy (Стратегия) выглядят практически одинаково (оба используют композицию и делегирование через общий интерфей...


🟢 Junior Level

С точки зрения структуры классов паттерны State (Состояние) и Strategy (Стратегия) выглядят практически одинаково (оба используют композицию и делегирование через общий интерфейс). Однако их архитектурное назначение (Intent) принципиально различается:

  • Strategy (Стратегия): алгоритм выбирается клиентом извне. Стратегии обычно не знают о существовании друг друга и решают одну задачу разными способами (например, разные методы сортировки или оплаты).
  • State (Состояние): поведение объекта меняется изнутри в зависимости от его текущего состояния в рамках жизненного цикла. Состояния часто знают о возможных переходах друг в друга (модель конечного автомата).

Наглядная аналогия

  • Strategy: Вы садитесь за руль автомобиля и выбираете режим коробки передач: «Спорт», «Эко» или «Комфорт». Вы сами решаете, какой алгоритм управления двигателем использовать.
  • State: Светофор. Он сам последовательно переключается: «Красный» $\rightarrow$ «Желтый» $\rightarrow$ «Зеленый». В зависимости от текущего цвета его реакция на внешние события меняется (при красном машины стоят, при зеленом — едут), и водитель не может произвольно включить зеленый цвет в обход логики автомата.

Быстрый пример кода

// Strategy: клиент сам решает, какую реализацию передать
Order order = new Order();
order.setPaymentStrategy(new CreditCardPayment()); // Внедрение извне
order.pay();

// State: клиент вызывает бизнес-действие, а объект сам переключает состояние
Order order = new Order(); // Внутри: State = NEW
order.processPayment();    // Внутри переключился на PAID
order.ship();              // Внутри переключился на SHIPPED
order.processPayment();    // ❌ Исключение: заказ уже оплачен!

🟡 Middle Level

Разбор паттерна State: Управление жизненным циклом и переходами

Паттерн State реализует концепцию конечного автомата (Finite State Machine, FSM):

// Интерфейс состояния
public interface DocumentState {
    void edit(Document doc, String text);
    void publish(Document doc);
}

// Контекст
public class Document {
    private DocumentState state;
    private String content = "";

    public Document() {
        this.state = new DraftState(); // Начальное состояние
    }

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

    public void edit(String text) { state.edit(this, text); }
    public void publish() { state.publish(this); }
    public void setContent(String content) { this.content = content; }
}

// Конкретные состояния
public class DraftState implements DocumentState {
    public void edit(Document doc, String text) {
        doc.setContent(text);
        System.out.println("Черновик отредактирован");
    }

    public void publish(Document doc) {
        System.out.println("Черновик отправлен на модерацию");
        doc.setState(new ModerationState()); // Переход в следующее состояние!
    }
}

public class PublishedState implements DocumentState {
    public void edit(Document doc, String text) {
        throw new IllegalStateException("Опубликованный документ нельзя редактировать!");
    }

    public void publish(Document doc) {
        System.out.println("Документ уже опубликован");
    }
}

Сравнительная таблица различий

Критерий Strategy (Стратегия) State (Состояние)
Кто управляет переключением? Внешний клиент (через конструктор или сеттер). Сам контекст или классы состояний по правилам переходов.
Осведомленность о сосе Neighbors Стратегии полностью изолированы и ничего не знают друг о друге. Состояния часто знают о других состояниях, так как инициируют переходы.
Мутабельность состояния Как правило, Stateless (чистый алгоритм). Экземпляр можно сделать Singleton. Stateful: инкапсулирует статус процесса, правила валидации и переходы.
Цель паттерна Устранить дублирование кода и ветвления при выборе альтернативных алгоритмов. Устранить спагетти из условных операторов проверки статуса (if (status == PAID)).

Проблема сильной связанности в State (Tight Coupling) и способы решения

Если конкретное состояние StateA напрямую создает new StateB(), возникает жесткая связность между классами. Способы решения:

  1. Переходы внутри Контекста: Контекст содержит таблицу допустимых переходов, а состояния лишь возвращают статус операции (Event/Trigger).
  2. Использование Enum State: Если состояний немного и они не требуют тяжелых зависимостей, реализация через enum с абстрактными методами является самой чистой в Java.
public enum OrderStatus {
    NEW {
        @Override
        public OrderStatus next() { return PAID; }
    },
    PAID {
        @Override
        public OrderStatus next() { return DELIVERED; }
    },
    DELIVERED {
        @Override
        public OrderStatus next() { throw new IllegalStateException("Заказ уже завершен"); }
    };

    public abstract OrderStatus next();
}

🔴 Senior Level

Многопоточность и состояние гонки в паттерне State

В высоконагруженных системах изменение состояния объекта несколькими потоками одновременно — классический источник критических багов (Double Spending, Race Condition при переходах):

  1. Неатомарная смена состояния: Если проверка условий и вызов doc.setState(...) происходят без синхронизации, два параллельных потока могут одновременно перевести один и тот же заказ в статус CANCELLED и PAID.
  2. Способы обеспечения потокобезопасности:
    • AtomicReference<State>: подходит для простых линейных автоматов без побочных эффектов с помощью compareAndSet().
    • Оптимистические блокировки в БД (@Version в JPA / Hibernate): при конкурентной смене статуса один из потоков получит OptimisticLockException и откатит транзакцию.
    • Паттерн State Machine с синхронизированной очередью событий.

Enterprise-решения: Spring Statemachine

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

@Configuration
@EnableStateMachine
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStatus, OrderEvent> {

    @Override
    public void configure(StateMachineTransitionConfigurer<OrderStatus, OrderEvent> transitions) throws Exception {
        transitions
            .withExternal()
                .source(OrderStatus.NEW).target(OrderStatus.PAID).event(OrderEvent.PAY)
                .guard(paymentValidationGuard()) // Проверка условий
                .action(sendPaymentReceiptAction()) // Побочный эффект
            .and()
            .withExternal()
                .source(OrderStatus.PAID).target(OrderStatus.SHIPPED).event(OrderEvent.SHIP);
    }
}

Преимущества State Machine фреймворков:

  • Декларативная таблица переходов.
  • Guards (Охранные условия): переход не произойдет, пока условие не вернет true.
  • Actions (Действия): автоматический вызов бизнес-логики при входе (entry), выходе (exit) или переходе.
  • Интеграция с распределенной персистентностью (сохранение состояния в Redis/Postgres).

🎯 Шпаргалка для интервью

30-секундный ответ (Elevator Pitch)

«Несмотря на визуальную идентичность диаграмм классов, ключевая разница между Strategy и State заключается в назначении и источнике управления. Strategy выбирается внешним клиентом для подмены взаимозаменяемого алгоритма решения задачи (стратегии не знают друг о друге и обычно не хранят состояние). State моделирует конечный автомат (FSM): объект меняет свое поведение самостоятельно изнутри по мере прохождения жизненного цикла, а состояния знают правила допустимых переходов. Для простых сценариев State реализуется через enum, а для сложных корпоративных процессов используют Spring Statemachine».


3–4 каверзных вопроса с ответами

  1. Могут ли классы состояний в паттерне State быть Singleton-объектами?
    • Ответ: Да, если конкретные классы состояний не хранят собственного изменяемого состояния (stateless), а получают контекст через аргументы методов (state.handle(Context ctx)). В таком случае экземпляры состояний кэшируются в виде констант или enum, что снижает нагрузку на сборщик мусора GC.
  2. Как избежать сильной связанности (tight coupling), когда состояния знают друг о друге при переходе?
    • Ответ: Перенести логику переходов в контекст или отдельный класс-медиатор (Transition Manager / Registry). Состояние в этом случае лишь сообщает контексту событие (Event/Trigger), а контекст по таблице переходов определяет следующее состояние.
  3. Что произойдет, если в многопоточной среде два потока одновременно вызовут смену состояния у одного объекта?
    • Ответ: Без синхронизации возникнет состояние гонки (Race Condition), из-за которого объект может перейти в недопустимое состояние или дважды выполнить побочный эффект (например, двойное списание средств). Для защиты используют явные блокировки, атомарные ссылки (AtomicReference) или оптимистическую блокировку на уровне базы данных (@Version в JPA).
  4. Почему для реализации сложных бизнес-процессов предпочитают Spring Statemachine вместо ручного паттерна State?
    • Ответ: Ручной паттерн State плохо масштабируется при появлении развилок, иерархических состояний (Sub-states), откатов по таймаутам и требований к аудиту. Spring Statemachine предоставляет готовый декларативный механизм с охранными условиями (Guards), транзакционными действиями (Actions) и сериализацией автомата в базу данных.

Типичные ошибки и Red Flags

  • 🚩 Утверждение, что Strategy и State — это одно и то же (фатальная ошибка на собеседовании, демонстрирующая непонимание архитектурного замысла).
  • 🚩 Клиентский код напрямую переключает состояния объекта (order.setState(new DeliveredState())) — это превращает State в хаотичную Strategy и ломает инварианты автомата.
  • 🚩 Создание тяжелых экземпляров состояний через оператор new на каждый переход в высоконагруженных циклах вместо переиспользования синглтонов или enum.
  • 🚩 Игнорирование многопоточной синхронизации при конкурентных переходах состояний.

Связанные темы