Стратегия

Стратегия (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, чтобы выбрать нужную. Для двух простых взаимоисключающих вариантов отдельные классы и интерфейс могут оказаться избыточной обвязкой по сравнению с одним условием.
Итог
Стратегия нужна, когда один и тот же контекст должен уметь работать по-разному в зависимости от подставленного алгоритма, а сам алгоритм должен оставаться независимым классом, а не веткой условия внутри контекста. В отличие от Состояния, где переходы происходят сами по себе, стратегию выбирает клиент — и остаётся тем, кто отвечает за этот выбор.