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

Коли використовувати Strategy?

4. Auto-registry у Spring через List 5. Context Object для передачі даних 6. Уникайте у Hot-path (>5 реалізацій — поріг HotSpot) 7. Template Method для частин алгоритму 8. Strat...

Мовні версії: English Russian Ukrainian

🟢 Junior Level

Strategy — патерн, який дозволяє обирати різні алгоритми для одного й того ж завдання.

Проста аналогія: Навігатор. Ціль одна (дістатися з A в B), але маршрути різні: на машині, пішки, громадським транспортом.

Приклад:

// Інтерфейс стратегії
interface PaymentStrategy {
    void pay(BigDecimal amount);
}

// Конкретні стратегії
class CreditCardStrategy implements PaymentStrategy {
    public void pay(BigDecimal amount) {
        System.out.println("Paid " + amount + " by credit card");
    }
}

class PayPalStrategy implements PaymentStrategy {
    public void pay(BigDecimal amount) {
        System.out.println("Paid " + amount + " via PayPal");
    }
}

// Використання
PaymentStrategy strategy = new CreditCardStrategy();
strategy.pay(new BigDecimal("100"));

// Легко поміняти
strategy = new PayPalStrategy();
strategy.pay(new BigDecimal("200"));

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

  • Є кілька способів зробити одне й те саме
  • Потрібно обирати алгоритм на runtime
  • Багато if-else або switch

🟡 Middle Level

Проблема: розростаючий switch

// ❌ Погано: додавання нового типу = зміна методу
public void processPayment(PaymentType type, BigDecimal amount) {
    switch (type) {
        case CREDIT_CARD:
            processCreditCard(amount);
            break;
        case PAYPAL:
            processPayPal(amount);
            break;
        case CRYPTO:
            processCrypto(amount);
            break;
        // Кожен новий тип → зміна методу!
    }
}

Рішення: Strategy

// ✅ Добре: додавання нового типу = новий клас
interface PaymentStrategy {
    void pay(BigDecimal amount);
}

class CreditCardPayment implements PaymentStrategy {
    public void pay(BigDecimal amount) { /* логіка */ }
}

class PayPalPayment implements PaymentStrategy {
    public void pay(BigDecimal amount) { /* логіка */ }
}

// Вибір стратегії
Map<PaymentType, PaymentStrategy> strategies = Map.of(
    CREDIT_CARD, new CreditCardPayment(),
    PAYPAL, new PayPalPayment(),
    CRYPTO, new CryptoPayment()
);

strategies.get(type).pay(amount);

Modern Java: лямбди

// Для простих стратегій не потрібні класи!
Map<PaymentType, Consumer<BigDecimal>> strategies = Map.of(
    CREDIT_CARD, amount -> processCreditCard(amount),
    PAYPAL, amount -> processPayPal(amount),
    CRYPTO, amount -> processCrypto(amount)
);

strategies.get(type).accept(amount);

Enum Strategy

// Для обмеженого набору стратегій
public enum ShippingStrategy {
    AIR {
        public BigDecimal calculate(BigDecimal weight) {
            return weight.multiply(BigDecimal.TEN);
        }
    },
    SEA {
        public BigDecimal calculate(BigDecimal weight) {
            return weight.multiply(BigDecimal.ONE);
        }
    };

    public abstract BigDecimal calculate(BigDecimal weight);
}

// Використання
BigDecimal cost = ShippingStrategy.AIR.calculate(weight);

Spring: Auto-registry Strategy

// Автоматична реєстрація всіх стратегій
public interface DiscountStrategy {
    BigDecimal apply(BigDecimal amount);
    String getType();
}

@Component
public class PercentageDiscount implements DiscountStrategy {
    public String getType() { return "PERCENTAGE"; }
    public BigDecimal apply(BigDecimal amount) { return amount.multiply(new BigDecimal("0.9")); }
}

@Component
public class FixedDiscount implements DiscountStrategy {
    public String getType() { return "FIXED"; }
    public BigDecimal apply(BigDecimal amount) { return amount.subtract(new BigDecimal("100")); }
}

// Автоматичний Map
@Service
public class DiscountService {
    private final Map<String, DiscountStrategy> strategies;

    public DiscountService(List<DiscountStrategy> allStrategies) {
        this.strategies = allStrategies.stream()
            .collect(Collectors.toMap(
                DiscountStrategy::getType,
                Function.identity()
            ));
    }

    public BigDecimal applyDiscount(String type, BigDecimal amount) {
        return strategies.get(type).apply(amount);
    }
}

// Додати нову стратегію = створити новий @Component!

Типові помилки

  1. Strategy для простого if-else
    // ❌ Overengineering
    interface CompareStrategy { int compare(int a, int b); }
    class AscendingCompare implements CompareStrategy { ... }
    class DescendingCompare implements CompareStrategy { ... }
    
    // ✅ Достатньо лямбди
    Comparator<Integer> asc = Integer::compare;
    Comparator<Integer> desc = (a, b) -> Integer.compare(b, a);
    
  2. Товстий інтерфейс
    // ❌ Занадто багато методів
    interface Strategy {
        void init();
        void process();
        void cleanup();
        String getName();
        int getPriority();
    }
    
    // ✅ Розділіть на різні стратегії
    

🔴 Senior Level

Strategy vs State vs Template Method

Strategy vs State:

// Strategy: вибір ззовні
order.setPaymentStrategy(new CreditCardPayment());  // Клієнт обирає

// State: зміна зсередини
order.setState(PaidState.INSTANCE);  // Об'єкт сам змінює стан
order.process();  // Поведінка залежить від стану

Strategy vs Template Method:

// Template Method: наслідування, частини алгоритму
abstract class AbstractReport {
    public void generate() {
        loadData();      // Перевизначається
        formatData();    // Перевизначається
        saveReport();    // Загальний код
    }
}

// Strategy: композиція, весь алгоритм
interface ReportGenerator { Report generate(); }
class PdfReportGenerator implements ReportGenerator { ... }
class HtmlReportGenerator implements ReportGenerator { ... }

JIT і поліморфізм

// Monomorphic call (1 реалізація) → інлайнинг
PaymentStrategy strategy = new CreditCardPayment();
strategy.pay(amount);  // JIT вбудовує код → 0 overhead

// Bimorphic call (2 реалізації) → inline cache
// JIT все ще оптимізує

// Megamorphic call (>5 реалізацій — поріг HotSpot) → vtable lookup
interface PaymentStrategy { void pay(); }
class A implements PaymentStrategy { ... }
class B implements PaymentStrategy { ... }
class C implements PaymentStrategy { ... }
class D implements PaymentStrategy { ... }  // >2!

// JIT не може інлайнити → непрямий виклик
// → ~5-10ns overhead на виклик
// → У Hot-path (1M викликів/сек) = 5-10ms втрати

Оптимізація для Hot-path:

// Замість інтерфейсу → enum switch
public enum PaymentType {
    CREDIT_CARD, PAYPAL, CRYPTO
}

public void process(PaymentType type, BigDecimal amount) {
    switch (type) {
        case CREDIT_CARD -> processCreditCard(amount);
        case PAYPAL -> processPayPal(amount);
        case CRYPTO -> processCrypto(amount);
    }
}

// → JIT компілює у tableswitch → O(1) → швидко!

Context Object Pattern

// Проблема: стратегія потребує багатьох даних
interface PricingStrategy {
    BigDecimal calculate(Product p, Customer c, Cart cart,
                        Promotion promo, Locale locale);
}

// Рішення: об'єкт-контекст
public record PricingContext(
    Product product,
    Customer customer,
    Cart cart,
    Promotion promotion,
    Locale locale
) {}

interface PricingStrategy {
    BigDecimal calculate(PricingContext context);
}

// Додавання нових даних не ламає інтерфейс!

Production Experience

Реальний сценарій #1: Megamorphic slowdown

  • 10 реалізацій DiscountStrategy
  • Hot-path: 100 000 викликів/сек
  • Проблема: virtual call overhead ~1-5ns (наносекунди), не мілісекунди
  • Рішення: enum switch замість інтерфейсу
  • Результат: 5ms (прискорення у 10 разів)

Реальний сценарій #2: Spring auto-registry

  • 20 стратегій валідації
  • Ручна реєстрація у Map → помилки
  • Рішення: авто-реєстрація через List injection
  • Результат: нова стратегія = новий @Component

Best Practices

  1. Лямбди для простих стратегій
  2. Enum для обмеженого набору
  3. Класи для складної логіки
  4. Auto-registry у Spring через List
  5. Context Object для передачі даних
  6. Уникайте у Hot-path (>5 реалізацій — поріг HotSpot)
  7. Template Method для частин алгоритму
  8. Strategy для всієї заміни алгоритму

Резюме для Senior

  • Strategy = заміна всього алгоритму через композицію
  • State = зміна поведінки зсередини
  • Template Method = заміна частин через наслідування
  • JIT: megamorphic calls >5 реалізацій (поріг HotSpot) → overhead
  • Hot-path: enum switch > інтерфейс
  • Context Object для передачі даних у стратегію
  • Spring auto-registry: List → Map
  • Modern Java: лямбди замінюють прості стратегії

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

Обов’язково знати:

  • Strategy замінює розростаючий switch/if-else через композицію — кожен алгоритм в окремому класі
  • У Modern Java лямбди замінюють прості стратегії: Map<Enum, Function> замість 10+ класів
  • Spring auto-registry: List injection → автоматичний Map<String, Strategy>
  • JIT оптимізація: megamorphic calls (>5 реалізацій — поріг HotSpot) → virtual call overhead ~5-10ns
  • Для Hot-path: enum switch швидший за інтерфейс (JIT компілює у tableswitch → O(1))
  • Context Object Pattern: передача даних у стратегію через об’єкт-контекст замість 5+ параметрів
  • Enum Strategy — для обмеженого набору алгоритмів з компактним кодом

Часті уточнювальні запитання:

  • Коли Strategy — overengineering? — Простий if-else з 2-3 гілками і нескладною логікою
  • Чим Strategy відрізняється від State? — Strategy обирається ззовні (клієнт), State змінюється зсередини (об’єкт сам)
  • Що таке megamorphic call? — Виклик методу з >5 реалізаціями (поріг HotSpot) → JIT не інлайнить → overhead
  • Як Spring автоматично реєструє стратегії? — List injection → Stream.collect(toMap)

Червоні прапорці (НЕ говорити):

  • “Я використовую Strategy для 2 гілок if-else” — overengineering, лямбди достатньо
  • “Strategy не впливає на продуктивність” — megamorphic calls >5 реалізацій measurable overhead
  • “Я не використовую Strategy, пишу switch” — порушення OCP, додавання нового типу = зміна коду
  • “Всі стратегії мають бути в окремих класах” — лямбди і enum замінюють прості стратегії

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

  • [[13. В чому різниця між State та Strategy]] — порівняння патернів
  • [[1. Що таке патерни проектування]] — лямбди замінюють Strategy
  • [[2. Які категорії патернів існують]] — Behavioral патерни
  • [[16. Які антипатерни ви знаєте]] — Golden Hammer
  • [[14. Які типи Proxy існують]] — альтернативний Structural підхід