🧩 Розділ 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 (Стан)
Хто керує перемиканням? Зовнішній клієнт (через конструктор або сетер). Сам контекст або класи станів за правилами переходів.
Обізнаність про сусідів Стратегії повністю ізольовані й нічого не знають одна про одну. Стани часто знають про інші стани, оскільки ініціюють переходи.
Мутабельність стану Як правило, 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):

import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;

@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()) // Перевірка умов (Guard)
                .action(sendPaymentReceiptAction()) // Побічний ефект (Action)
            .and()
            .withExternal()
                .source(OrderStatus.PAID).target(OrderStatus.SHIPPED).event(OrderEvent.SHIP);
    }
}

Переваги State Machine фреймворків:

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

🎯 Шпаргалка для інтерв’ю

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.
  • 🚩 Ігнорування багатопотокової синхронізації при конкурентних переходах між станами.

Пов’язані теми