Когда использовать 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 SbpPaymentStrategy implements PaymentStrategy {
public void pay(BigDecimal amount) {
System.out.println("Оплата по СБП (QR-код): " + 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 с абстрактными методами:
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>):
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<String, Strategy> или List<Strategy>:
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) { /* отправка в TG */ }
}
@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 конкретная реализация): JIT-компилятор (C2) полностью инлайнит тело стратегии в вызывающий код. Стоимость вызова равна нулю (0 ns overhead).
- 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):
public record ExecutionContext(
String orderId,
BigDecimal amount,
String currency,
UserInfo user,
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()vsComparator.reverseOrder()).
- Ответ: Когда логика состоит всего из двух простых тривиальных условий, которые вряд ли будут расширяться (например, обычный флаг
- Что такое мегаморфный вызов (Megamorphic Call) в контексте паттерна Strategy?
- Ответ: Это ситуация, когда в одной точке вызова метода интерфейса стратегии JVM встречает более двух различных классов-реализаций. В этом случае JIT-компилятор HotSpot не может выполнить инлайнинг и вынужден производить медленный поиск по таблице виртуальных методов (
vtable dispatch).
- Ответ: Это ситуация, когда в одной точке вызова метода интерфейса стратегии JVM встречает более двух различных классов-реализаций. В этом случае JIT-компилятор HotSpot не может выполнить инлайнинг и вынужден производить медленный поиск по таблице виртуальных методов (
Типичные ошибки и Red Flags
- 🚩 Создание десятков классов для однострочных операций, которые легко выражаются стандартными лямбдами (
Function,Predicate). - 🚩 Хранение мутабельного состояния внутри экземпляров стратегий (стратегия должна быть stateless, чтобы безопасно использоваться в многопоточной среде).
- 🚩 Перегрузка сигнатуры метода стратегии десятками параметров вместо передачи единого
Context Object. - 🚩 Ручной оператор
newдля стратегий в Spring-приложении вместо использования инъекции зависимостей.
Связанные темы
- В чём разница между State и Strategy — детальное сопоставление паттернов
- Что такое паттерны проектирования — основы классификации GoF
- Какие категории паттернов существуют — поведенческие шаблоны
- Зачем вообще нужны принципы SOLID — принцип Open/Closed (OCP)
- Какие антипаттерны вы знаете — спагетти-код и разрастающиеся switch