У чому різниця між 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(), виникає жорстка зв’язаність між класами. Способи вирішення:
- Переходи всередині Контексту: Контекст зберігає таблицю дозволених переходів, а стани лише повертають статус операції (
Event/Trigger). - Використання 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 під час переходів):
- Неатомарна зміна стану:
Якщо перевірка умов і виклик
doc.setState(...)відбуваються без синхронізації, два паралельні потоки можуть одночасно перевести одне й те саме замовлення в статусCANCELLEDтаPAID. - Способи забезпечення потокобезпечності:
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 каверзних запитання з відповідями
- Чи можуть класи станів у патерні State бути Singleton-об’єктами?
- Відповідь: Так, якщо конкретні класи станів не зберігають власного змінного стану (тобто є stateless), а отримують контекст через параметри методів (
state.handle(Context ctx)). У такому разі екземпляри станів кешуються у вигляді констант або елементівenum, що суттєво знижує навантаження на збирач сміття GC.
- Відповідь: Так, якщо конкретні класи станів не зберігають власного змінного стану (тобто є stateless), а отримують контекст через параметри методів (
- Як уникнути сильної зв’язаності (tight coupling), коли стани знають один про одного під час переходу?
- Відповідь: Перенести логіку переходів у сам контекст або окремий клас-медіатор (Transition Manager / Registry). Стан у такому випадку лише повідомляє контексту подію (Event/Trigger), а контекст за таблицею переходів самостійно визначає наступний цільовий стан.
- Що станеться, якщо в багатопотоковому середовищі два потоки одночасно викличуть зміну стану в одного об’єкта?
- Відповідь: Без належної синхронізації виникне стан гонки (Race Condition), через який об’єкт може перейти в недопустимий стан або двічі виконати критичний побічний ефект (наприклад, подвійне списання коштів). Для захисту застосовують явні блокування, атомарні посилання (
AtomicReference) або оптимістичне блокування на рівні БД (@Versionу JPA).
- Відповідь: Без належної синхронізації виникне стан гонки (Race Condition), через який об’єкт може перейти в недопустимий стан або двічі виконати критичний побічний ефект (наприклад, подвійне списання коштів). Для захисту застосовують явні блокування, атомарні посилання (
- Чому для реалізації складних бізнес-процесів надають перевагу Spring Statemachine замість власноруч написаного патерна State?
- Відповідь: Ручний патерн State погано масштабується при появі розгалужень, ієрархічних підстанів (Sub-states), переходів за таймаутами та вимог до аудиту. Spring Statemachine надає готовий декларативний механізм з охоронними умовами (Guards), транзакційними діями (Actions) та автоматичною серіалізацією автомата в базу даних.
Типові помилки та Red Flags
- 🚩 Твердження, що Strategy і State — це одне й те саме (груба помилка на співбесіді, що демонструє нерозуміння архітектурного задуму патернів).
- 🚩 Клієнтський код безпосередньо перемикає стани об’єкта (
order.setState(new DeliveredState())) — це перетворює State на хаотичну Strategy і повністю руйнує інваріанти автомата. - 🚩 Створення важких екземплярів станів через оператор
newна кожен перехід у високонавантажених циклах замість перевикористання синглтонів або enum. - 🚩 Ігнорування багатопотокової синхронізації при конкурентних переходах між станами.
Пов’язані теми
- Коли використовувати Strategy — детальний розбір патерна Strategy
- Які категорії патернів існують — класифікація поведінкових шаблонів GoF
- Що таке патерни проектування — основи патернів проєктування
- Що таке транзакції — атомарність зміни станів
- Hibernate JPA — мапінг станів та аннотація
@Enumerated