Перейти к содержимому
Фёдор Башинский

Стратегия

5 min read
Обложка статьи «Стратегия»

Стратегия (Strategy) — это поведенческий паттерн проектирования, который выносит семейство взаимозаменяемых алгоритмов в отдельные классы с общим интерфейсом и позволяет подставлять нужный алгоритм в контекст, не разрастая его условиями по типу расчёта.

Проблема

Расчёт стоимости ремонта зависит от типа клиента: обычная цена, скидка для корпоративного клиента, покрытие баллами по программе лояльности. Всё это реализовано одним методом:

final class RepairEstimate
{
    public function calculateTotal(int $baseAmount, string $clientType, int $loyaltyPoints = 0): int
    {
        return match ($clientType) {
            'standard' => $baseAmount,
            'corporate' => (int) round($baseAmount * 0.85), // корпоративная скидка 15%
            'loyalty' => max(0, $baseAmount - $loyaltyPoints), // баллы гасят стоимость напрямую
        };
    }
}

Условия усложняются. Оказывается, единой корпоративной скидки в 15% не существует — у разных корпоративных клиентов свой процент, согласованный по контракту. А баллы лояльности не должны покрывать больше 30% суммы, чтобы ремонт не оказывался бесплатным целиком. Каждое такое уточнение добавляет новый параметр в calculateTotal() и новую ветку внутрь match — метод, отвечающий за расчёт итоговой суммы, постепенно вбирает в себя внутренние бизнес-правила трёх никак не связанных друг с другом программ ценообразования. Протестировать расчёт баллов лояльности отдельно от корпоративной скидки уже не получится — они сидят в одном методе с общим набором параметров.

Решение

Стратегия выносит каждое правило ценообразования в свой класс за общим интерфейсом:

interface PricingStrategy
{
    public function calculate(int $baseAmount): int;
}
 
final class StandardPricing implements PricingStrategy
{
    public function calculate(int $baseAmount): int
    {
        return $baseAmount;
    }
}
 
final class CorporatePricing implements PricingStrategy
{
    public function __construct(
        private readonly float $discountRate, // индивидуальный процент по контракту
    ) {}
 
    public function calculate(int $baseAmount): int
    {
        return (int) round($baseAmount * (1 - $this->discountRate));
    }
}
 
final class LoyaltyPricing implements PricingStrategy
{
    private const MAX_COVERAGE_RATE = 0.3;
 
    public function __construct(
        private readonly int $availablePoints,
    ) {}
 
    public function calculate(int $baseAmount): int
    {
        $maxCoverage = (int) round($baseAmount * self::MAX_COVERAGE_RATE);
        $coverage = min($this->availablePoints, $maxCoverage);
 
        return $baseAmount - $coverage;
    }
}

RepairEstimate становится контекстом — работает со стратегией через интерфейс, не зная, что происходит внутри конкретной реализации:

final class RepairEstimate
{
    public function __construct(
        private readonly PricingStrategy $pricing,
    ) {}
 
    public function calculateTotal(int $baseAmount): int
    {
        return $this->pricing->calculate($baseAmount);
    }
}
$estimate = new RepairEstimate(new CorporatePricing(discountRate: 0.2));
$estimate->calculateTotal(40_000); // 32 000 — правило скидки живёт отдельно от RepairEstimate
 
$estimate = new RepairEstimate(new LoyaltyPricing(availablePoints: 50_000));
$estimate->calculateTotal(40_000); // баллы покроют не больше 30% — правило изолировано в LoyaltyPricing

Каждое правило ценообразования теперь тестируется в изоляции от остальных, а появление четвёртой программы — например, промо-акции — означает новый класс, а не новую ветку внутри уже перегруженного метода.

Из чего состоит паттерн

  • Стратегия (PricingStrategy) — общий интерфейс семейства алгоритмов.
  • Конкретная стратегия (StandardPricing, CorporatePricing, LoyaltyPricing) — одна конкретная реализация алгоритма.
  • Контекст (RepairEstimate) — работает со стратегией через интерфейс, не зная деталей конкретной реализации.
  • Клиент — выбирает нужную стратегию и передаёт её в контекст.

Стратегия vs Состояние

Структурно паттерны совпадают — интерфейс и семейство подключаемых через композицию реализаций, — но задачи у них разные. Стратегию клиент выбирает один раз, обычно при создании контекста, и сами стратегии не знают друг о друге и не переключают одна другую: CorporatePricing понятия не имеет о существовании LoyaltyPricing. Состояние (см. отдельную статью) устроено наоборот — конкретные состояния явно знают о соседних состояниях и сами переключают на них контекст по мере вызова его методов, моделируя конечный автомат, а не разовый выбор алгоритма.

СтратегияСостояние
Кто выбирает реализациюКлиент, обычно один раз при сборке контекстаТекущее состояние само переключает контекст на следующее
Знают ли реализации друг о другеНет — стратегии полностью независимыДа — состояние явно знает, в какое состояние перейти дальше
ЗадачаПодменить один взаимозаменяемый алгоритм другимСмоделировать поведение, зависящее от внутренней истории объекта

Стратегия vs Шаблонный метод

Оба паттерна позволяют варьировать часть алгоритма, не дублируя остальное, но делают это разными механизмами. Шаблонный метод (см. отдельную статью) фиксирует общий скелет алгоритма в базовом классе через наследование, и меняются только отдельные шаги, переопределённые в подклассе, — выбор варианта происходит один раз, на этапе выбора конкретного класса, и не меняется во время выполнения. Стратегия подставляет алгоритм целиком через композицию — контекст хранит объект-стратегию и может подменить его в любой момент, включая рантайм, если стратегия выбирается динамически, например, по данным клиента, а не жёстко на этапе написания кода.

СтратегияШаблонный метод
МеханизмКомпозиция — контекст хранит объект-стратегиюНаследование — шаги переопределяются в подклассе
Что варьируетсяАлгоритм целиком, подставляется объектомОтдельные шаги внутри фиксированного скелета
Когда выбирается вариантВо время выполнения, можно менять на летуПри создании конкретного подкласса, фиксировано в коде

Когда применять

  • Есть несколько взаимозаменяемых способов выполнить одну и ту же задачу, которые должны выбираться независимо от того, кто их использует.
  • Нужно избавиться от ветвления по типу клиента, режима или условия внутри одного метода, как в примере с расчётом стоимости ремонта.
  • Алгоритм нужно менять во время выполнения — в зависимости от входных данных, а не фиксировать один раз при написании кода.

Плюсы и минусы

Стратегия изолирует каждый алгоритм в собственном тестируемом классе, а новый вариант добавляется новым классом, не трогая ни контекст, ни уже существующие стратегии. Ветвление по типу расчёта уходит из клиентского кода и контекста целиком.

Минус — сам выбор стратегии никуда не девается, паттерн лишь переносит его туда, где создаётся контекст: клиент по-прежнему должен знать разницу между StandardPricing, CorporatePricing и LoyaltyPricing, чтобы выбрать нужную. Для двух простых взаимоисключающих вариантов отдельные классы и интерфейс могут оказаться избыточной обвязкой по сравнению с одним условием.

Итог

Стратегия нужна, когда один и тот же контекст должен уметь работать по-разному в зависимости от подставленного алгоритма, а сам алгоритм должен оставаться независимым классом, а не веткой условия внутри контекста. В отличие от Состояния, где переходы происходят сами по себе, стратегию выбирает клиент — и остаётся тем, кто отвечает за этот выбор.