Коли використовувати Strategy
Поїздка в аеропорт: дістатися до термінала можна на таксі, експрес-потягом або на орендованому авто (каршеринг). Мета одна й та сама (доїхати), але алгоритм руху, час і вартість...
🟢 Junior Level
Strategy (Стратегія) — це поведінковий патерн проєктування, який визначає сімейство схожих алгоритмів, інкапсулює кожен із них в окремий клас і робить їх взаємозамінними під час виконання програми (runtime).
Проста аналогія
Поїздка в аеропорт: дістатися до термінала можна на таксі, експрес-потягом або на орендованому авто (каршеринг). Мета одна й та сама (доїхати), але алгоритм руху, час і вартість різняться. Ви обираєте відповідний «спосіб» (стратегію) безпосередньо перед виїздом залежно від бюджету та заторів на дорогах.
Базовий приклад реалізації
import java.math.BigDecimal;
// 1. Спільний інтерфейс стратегії
public interface PaymentStrategy {
void pay(BigDecimal amount);
}
// 2. Конкретні стратегії
public class CardPaymentStrategy implements PaymentStrategy {
public void pay(BigDecimal amount) {
System.out.println("Оплата банківською карткою: " + amount + " грн.");
}
}
public class ApplePayPaymentStrategy implements PaymentStrategy {
public void pay(BigDecimal amount) {
System.out.println("Оплата через Apple Pay: " + amount + " грн.");
}
}
// 3. Контекст, що використовує стратегію
public class OrderCheckout {
private PaymentStrategy strategy;
public void setStrategy(PaymentStrategy strategy) {
this.strategy = strategy;
}
public void completeOrder(BigDecimal total) {
if (strategy == null) {
throw new IllegalStateException("Стратегію оплати не обрано!");
}
strategy.pay(total);
}
}
Коли використовувати:
- У коді є безліч споріднених класів, які відрізняються виключно поведінкою (алгоритмом).
- Потрібно динамічно змінювати алгоритм роботи об’єкта під час виконання програми.
- Код перевантажений розгалуженнями
if-elseабо громіздкими конструкціямиswitch, які перевіряють тип операції.
🟡 Middle Level
Усунення антипатерна switch та дотримання Open/Closed Principle (OCP)
У процедурному стилі вибір алгоритму найчастіше оформлюють через switch:
// ❌ Порушення принципу OCP: при додаванні нового типу доставки доведеться змінювати цей клас!
public class DeliveryPriceCalculator {
public BigDecimal calculateCost(DeliveryType type, Parcel parcel) {
switch (type) {
case COURIER:
return parcel.getWeight().multiply(new BigDecimal("150"));
case POST:
return parcel.getWeight().multiply(new BigDecimal("50"));
case PICKUP_POINT:
return new BigDecimal("100");
default:
throw new IllegalArgumentException("Невідомий тип доставки: " + type);
}
}
}
Патерн Strategy переносить кожну гілку switch в окремий незалежний клас. Додавання нового способу доставки (наприклад, DRONE) вимагає створення нового класу без зміни вже існуючого перевіреного коду.
Реалізація через Enum зі стратегіями (Enum Strategy)
Якщо набір алгоритмів фіксований і відомий наперед, елегантним рішенням є enum з абстрактними методами:
import java.math.BigDecimal;
public enum DiscountStrategy {
NO_DISCOUNT {
@Override
public BigDecimal apply(BigDecimal price) {
return price;
}
},
VIP_CLIENT {
@Override
public BigDecimal apply(BigDecimal price) {
return price.multiply(new BigDecimal("0.85")); // Знижка 15%
}
},
PROMO_CODE {
@Override
public BigDecimal apply(BigDecimal price) {
return price.multiply(new BigDecimal("0.90")); // Знижка 10%
}
};
public abstract BigDecimal apply(BigDecimal price);
}
Modern Java: Функціональні інтерфейси та Лямбди
У сучасній Java 8+ немає необхідності плодити десятки дрібних класів для простих стратегій. Достатньо використовувати стандартні функціональні інтерфейси (Function<T, R>, Predicate<T>, UnaryOperator<T>):
import java.util.function.UnaryOperator;
public class TextFormatter {
public static String format(String text, UnaryOperator<String> strategy) {
return strategy.apply(text);
}
}
// Використання через посилання на методи та лямбда-вирази:
String upper = TextFormatter.format("hello", String::toUpperCase);
String trimmed = TextFormatter.format(" hello ", String::strip);
Spring Framework: Автоматичний реєстр стратегій (Strategy Auto-Registry)
У Spring Boot стратегії позначаються анотацією @Component, а сервіс-контекст автоматично інжектує їх у Map<Key, Strategy> або List<Strategy>:
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
public interface NotificationSender {
NotificationChannel getChannel();
void send(String recipient, String message);
}
@Component
public class TelegramNotificationSender implements NotificationSender {
public NotificationChannel getChannel() { return NotificationChannel.TELEGRAM; }
public void send(String to, String msg) { /* логіка відправки в Telegram */ }
}
@Component
public class EmailNotificationSender implements NotificationSender {
public NotificationChannel getChannel() { return NotificationChannel.EMAIL; }
public void send(String to, String msg) { /* логіка відправки Email */ }
}
@Service
public class NotificationRoutingService {
private final Map<NotificationChannel, NotificationSender> senderRegistry;
// Spring автоматично інжектує ВСІ біни, що реалізують інтерфейс NotificationSender
public NotificationRoutingService(List<NotificationSender> senders) {
this.senderRegistry = senders.stream()
.collect(Collectors.toMap(NotificationSender::getChannel, Function.identity()));
}
public void routeNotification(NotificationChannel channel, String to, String msg) {
NotificationSender sender = senderRegistry.get(channel);
if (sender == null) {
throw new UnsupportedOperationException("Канал сповіщень не підтримується: " + channel);
}
sender.send(to, msg);
}
}
🔴 Senior Level
Вплив на JIT-компіляцію: Поліморфізм викликів та Megamorphic Inlining
Під час виклику методу стратегії через загальний інтерфейс віртуальна машина HotSpot JVM відстежує статистику викликів (Inline Cache):
- Monomorphic Call (1 конкретна реалізація в runtime): JIT-компілятор (C2) повністю інлайнить тіло стратегії в код, що її викликає. Вартість виклику дорівнює нулю (0 ns накладних витрат).
- Bimorphic Call (2 реалізації): Компілятор генерує ефективну умовну машинну команду (checkcast) та інлайнить обидві гілки коду.
- Megamorphic Call (>2 реалізацій у точці виклику): У класичному HotSpot компілятор вимикає інлайнінг, якщо в одній точці виклику фіксується більше двох різних класів. Виклик виконується через перегляд таблиці віртуальних методів (vtable lookup), що додає ~5–10 ns оверхеду на непряму адресацію та скидає конвеєр інструкцій CPU.
⚡ Оптимізація для високочастотних ділянок коду (Hot-Path): Якщо у високочастотному циклі (HFT, ігрові рушії) потрібно перебирати десятки стратегій, віртуальні виклики інтерфейсу можуть деградувати пропускну здатність. У таких сценаріях
switchзаenumтранслюється в байткод-інструкціюtableswitch(прямий перехід за зміщенням O(1)), яка працює швидше за мегаморфний інтерфейсний виклик.
Архітектурне зіставлення: Strategy vs State vs Template Method
| Патерн | Основний механізм | Хто керує перемиканням? | Ступінь підміни |
|---|---|---|---|
| Strategy | Композиція (делегування) | Клієнтський код ззовні впроваджує потрібний алгоритм. | Повна підміна всього алгоритму цілком. |
| State | Композиція (делегування) | Сам об’єкт змінює свій стан зсередини в міру проходження життєвого циклу. | Поведінка об’єкта змінюється залежно від його поточного внутрішнього стану. |
| Template Method | Наслідування (базовий клас) | Базовий клас жорстко задає каркас алгоритму. | Підкласи перевизначають окремі кроки скелета, але не можуть змінити загальну структуру. |
Патерн Context Object для чистих сигнатур
Якщо різні стратегії вимагають різних наборів вхідних параметрів, інтерфейс стратегії ризикує перетворитися на антипатерн «звалище аргументів»:
// ❌ Погано: частині стратегій взагалі не потрібні orderId або currency
void execute(String id, BigDecimal amt, String currency, String user, String ip);
Рішення: використання незмінного об’єкта контексту (наприклад, за допомогою Java record):
import java.math.BigDecimal;
import java.util.Map;
public record ExecutionContext(
String orderId,
BigDecimal amount,
String currency,
String username,
Map<String, Object> metadata
) {}
public interface ExecutionStrategy {
ExecutionResult process(ExecutionContext ctx);
}
🎯 Шпаргалка для інтерв’ю
30-секундна відповідь (Elevator Pitch)
«Патерн Strategy (Стратегія) інкапсулює взаємозамінні алгоритми розв’язання однієї задачі в окремі класи, об’єднані спільним інтерфейсом, дозволяючи перемикати поведінку під час виконання програми (runtime). Він усуває антипатерн розгалужених блоків
switch/if-elseта суворо дотримується принципу Open/Closed (OCP): для додавання нової бізнес-логіки достатньо створити новий клас, не змінюючи наявний код. У сучасному Java-стеку стратегії реалізуються через функціональні інтерфейси (лямбди), enum із методами або автоматично реєструються в Spring через інжекціюList<Strategy>уMap».
3–4 каверзних запитання з відповідями
- Чим патерн Strategy принципово відрізняється від State?
- Відповідь: Різниця в джерелі перемикання та призначенні. У Strategy алгоритм обирається зовнішнім клієнтом (наприклад, користувач обирає платіжний метод), а самі стратегії зазвичай не знають одна про одну та є безстанними (stateless). У State об’єкт самостійно змінює свої внутрішні стани залежно від подій життєвого циклу (наприклад,
Draft -> Review -> Published).
- Відповідь: Різниця в джерелі перемикання та призначенні. У Strategy алгоритм обирається зовнішнім клієнтом (наприклад, користувач обирає платіжний метод), а самі стратегії зазвичай не знають одна про одну та є безстанними (stateless). У State об’єкт самостійно змінює свої внутрішні стани залежно від подій життєвого циклу (наприклад,
- Як реалізувати патерн Strategy у Spring Boot без ручного написання фабрик?
- Відповідь: Достатньо позначити класи стратегій анотацією
@Componentі впровадити в цільовий сервісList<MyStrategy>абоMap<String, MyStrategy>. Spring автоматично виявить усі реалізації в контексті й збере їх у реєстр за ключами або типами.
- Відповідь: Достатньо позначити класи стратегій анотацією
- Коли використання Strategy є надлишковим (Overengineering)?
- Відповідь: Коли логіка складається всього з двох простих тривіальних умов, які навряд чи будуть розширюватися (наприклад, звичайний прапорець
boolean isAscending). У такому разі виділення інтерфейсу та окремих класів — зайве ускладнення архітектури; достатньо тернарного оператора або посилань на готові методи (Comparator.naturalOrder()протиComparator.reverseOrder()).
- Відповідь: Коли логіка складається всього з двох простих тривіальних умов, які навряд чи будуть розширюватися (наприклад, звичайний прапорець
- Що таке мегаморфний виклик (Megamorphic Call) у контексті патерна Strategy?
- Відповідь: Це ситуація, коли в одній точці виклику методу інтерфейсу стратегії JVM фіксує більше двох різних класів-реалізацій. У цьому випадку JIT-компілятор HotSpot не може виконати інлайнінг і змушений виконувати повільніший пошук за таблицею віртуальних методів (
vtable dispatch).
- Відповідь: Це ситуація, коли в одній точці виклику методу інтерфейсу стратегії JVM фіксує більше двох різних класів-реалізацій. У цьому випадку JIT-компілятор HotSpot не може виконати інлайнінг і змушений виконувати повільніший пошук за таблицею віртуальних методів (
Типові помилки та Red Flags
- 🚩 Створення десятків класів для однорядкових операцій, які легко виражаються стандартними лямбдами (
Function,Predicate,UnaryOperator). - 🚩 Зберігання мутабельного стану всередині екземплярів стратегій (стратегія зобов’язана бути stateless, щоб безпечно працювати в багатопотоковому середовищі).
- 🚩 Перевантаження сигнатури методу стратегії десятками необов’язкових параметрів замість передачі єдиного
Context Object. - 🚩 Ручний виклик оператора
newдля стратегій у Spring-застосунках замість використання механізму впровадження залежностей.
Пов’язані теми
- У чому різниця між State та Strategy — детальне зіставлення поведінкових патернів
- Що таке патерни проектування — основи класифікації GoF
- Які категорії патернів існують — поведінкові патерни
- Застосування SOLID принципів — принцип відкритості/закритості (OCP)
- Які антипатерни ви знаєте — спагеті-код і розростання switch