Когда использовать Strategy?
4. Auto-registry в Spring через List 5. Context Object для передачи данных 6. Избегайте в Hot-path (>5 реализаций — порог HotSpot) 7. Template Method для частей алгоритма 8. Str...
🟢 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 (nanoseconds), не миллисекунды
- Решение: enum switch вместо интерфейса
- Результат: 5ms (ускорение в 10 раз)
Реальный сценарий #2: Spring auto-registry
- 20 стратегий валидации
- Ручная регистрация в Map → ошибки
- Решение: авто-registry через 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 подход