🧩 Розділ 2 · Питання #10

Коли використовувати 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);
    }
}

Коли використовувати:

  1. У коді є безліч споріднених класів, які відрізняються виключно поведінкою (алгоритмом).
  2. Потрібно динамічно змінювати алгоритм роботи об’єкта під час виконання програми.
  3. Код перевантажений розгалуженнями 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):

  1. Monomorphic Call (1 конкретна реалізація в runtime): JIT-компілятор (C2) повністю інлайнить тіло стратегії в код, що її викликає. Вартість виклику дорівнює нулю (0 ns накладних витрат).
  2. Bimorphic Call (2 реалізації): Компілятор генерує ефективну умовну машинну команду (checkcast) та інлайнить обидві гілки коду.
  3. 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 каверзних запитання з відповідями

  1. Чим патерн Strategy принципово відрізняється від State?
    • Відповідь: Різниця в джерелі перемикання та призначенні. У Strategy алгоритм обирається зовнішнім клієнтом (наприклад, користувач обирає платіжний метод), а самі стратегії зазвичай не знають одна про одну та є безстанними (stateless). У State об’єкт самостійно змінює свої внутрішні стани залежно від подій життєвого циклу (наприклад, Draft -> Review -> Published).
  2. Як реалізувати патерн Strategy у Spring Boot без ручного написання фабрик?
    • Відповідь: Достатньо позначити класи стратегій анотацією @Component і впровадити в цільовий сервіс List<MyStrategy> або Map<String, MyStrategy>. Spring автоматично виявить усі реалізації в контексті й збере їх у реєстр за ключами або типами.
  3. Коли використання Strategy є надлишковим (Overengineering)?
    • Відповідь: Коли логіка складається всього з двох простих тривіальних умов, які навряд чи будуть розширюватися (наприклад, звичайний прапорець boolean isAscending). У такому разі виділення інтерфейсу та окремих класів — зайве ускладнення архітектури; достатньо тернарного оператора або посилань на готові методи (Comparator.naturalOrder() проти Comparator.reverseOrder()).
  4. Що таке мегаморфний виклик (Megamorphic Call) у контексті патерна Strategy?
    • Відповідь: Це ситуація, коли в одній точці виклику методу інтерфейсу стратегії JVM фіксує більше двох різних класів-реалізацій. У цьому випадку JIT-компілятор HotSpot не може виконати інлайнінг і змушений виконувати повільніший пошук за таблицею віртуальних методів (vtable dispatch).

Типові помилки та Red Flags

  • 🚩 Створення десятків класів для однорядкових операцій, які легко виражаються стандартними лямбдами (Function, Predicate, UnaryOperator).
  • 🚩 Зберігання мутабельного стану всередині екземплярів стратегій (стратегія зобов’язана бути stateless, щоб безпечно працювати в багатопотоковому середовищі).
  • 🚩 Перевантаження сигнатури методу стратегії десятками необов’язкових параметрів замість передачі єдиного Context Object.
  • 🚩 Ручний виклик оператора new для стратегій у Spring-застосунках замість використання механізму впровадження залежностей.

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