🧩 Раздел 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 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);
    }
}

Когда использовать:

  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 с абстрактными методами:

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):

  1. Monomorphic Call (1 конкретная реализация): JIT-компилятор (C2) полностью инлайнит тело стратегии в вызывающий код. Стоимость вызова равна нулю (0 ns overhead).
  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):

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 каверзных вопроса с ответами

  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() vs Comparator.reverseOrder()).
  4. Что такое мегаморфный вызов (Megamorphic Call) в контексте паттерна Strategy?
    • Ответ: Это ситуация, когда в одной точке вызова метода интерфейса стратегии JVM встречает более двух различных классов-реализаций. В этом случае JIT-компилятор HotSpot не может выполнить инлайнинг и вынужден производить медленный поиск по таблице виртуальных методов (vtable dispatch).

Типичные ошибки и Red Flags

  • 🚩 Создание десятков классов для однострочных операций, которые легко выражаются стандартными лямбдами (Function, Predicate).
  • 🚩 Хранение мутабельного состояния внутри экземпляров стратегий (стратегия должна быть stateless, чтобы безопасно использоваться в многопоточной среде).
  • 🚩 Перегрузка сигнатуры метода стратегии десятками параметров вместо передачи единого Context Object.
  • 🚩 Ручной оператор new для стратегий в Spring-приложении вместо использования инъекции зависимостей.

Связанные темы