Коли використовувати Strategy?
4. Auto-registry у Spring через List 5. Context Object для передачі даних 6. Уникайте у Hot-path (>5 реалізацій — поріг HotSpot) 7. Template Method для частин алгоритму 8. Strat...
🟢 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!
Типові помилки
- 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); - Товстий інтерфейс
// ❌ Занадто багато методів 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
- Лямбди для простих стратегій
- Enum для обмеженого набору
- Класи для складної логіки
- Auto-registry у Spring через List
- Context Object для передачі даних
- Уникайте у Hot-path (>5 реалізацій — поріг HotSpot)
- Template Method для частин алгоритму
- 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 підхід