Паттерн «Стратегия»: замените гигантские if-else одним элегантным решением
Разбираем поведенческий паттерн Strategy на примерах на Python и Java: когда он нужен, как его реализовать и чем он отличается от простого полиморфизма.
Содержание
Паттерн «Стратегия»: замените гигантские if-else одним элегантным решением
Каждый разработчик рано или поздно сталкивается с одним и тем же симптомом: метод, который начинался как десять строк, разрастается до ста, потом до трёхсот. Внутри — нагромождение if-else или switch-case, которые проверяют тип операции, роль пользователя, режим работы системы. Добавить новый вариант поведения означает залезть внутрь этого метода, рискуя сломать всё, что уже работает.
Паттерн Strategy — один из классических ответов на эту проблему. Он описан в книге «банды четырёх» (GoF) ещё в 1994 году, но остаётся актуальным и сегодня — именно потому, что решает фундаментальную проблему проектирования, а не синтаксическую.
В этой статье разберём, что такое Strategy по существу, когда его стоит применять, как реализовать на Python и Java, и чем он отличается от обычного полиморфизма — потому что это различие понимают далеко не все.
Проблема: когда код начинает «знать слишком много»
Представьте систему обработки платежей. Поначалу она поддерживает только банковские карты. Потом добавляется PayPal. Потом криптовалюта. Потом внутренний баланс. Каждый раз разработчик открывает метод process_payment и дописывает очередную ветку:
def process_payment(amount, method):
if method == "card":
# логика для карты
charge_card(amount)
elif method == "paypal":
# логика для PayPal
call_paypal_api(amount)
elif method == "crypto":
# логика для крипты
broadcast_transaction(amount)
elif method == "balance":
# логика для баланса
deduct_from_balance(amount)
else:
raise ValueError(f"Unknown payment method: {method}")
На первый взгляд — ничего страшного. Но у этого подхода есть системные проблемы:
- Нарушение принципа открытости/закрытости (OCP): каждое новое требование заставляет модифицировать существующий код.
- Сложность тестирования: чтобы протестировать логику крипты, нужно инициализировать весь метод целиком.
- Связанность: логика разных способов оплаты перемешана в одном месте. Ошибка в ветке
paypalможет повлиять на соседние ветки при рефакторинге. - Масштабирование в команде: два разработчика не могут одновременно работать над разными ветками без конфликтов в git.
Именно здесь Strategy предлагает принципиально другой подход.
Суть паттерна Strategy
Идея проста: вынести каждый вариант поведения в отдельный класс, объединив их общим интерфейсом. Контекст (класс, который использует поведение) работает только с этим интерфейсом и не знает, какая конкретная реализация подставлена.
Структура паттерна состоит из трёх элементов:
- Strategy (интерфейс/абстрактный класс) — объявляет метод, который реализуют все конкретные стратегии.
- ConcreteStrategy — конкретные реализации алгоритма.
- Context — класс, который хранит ссылку на стратегию и делегирует ей выполнение.
Ключевая идея: поведение становится объектом, которым можно управлять — передавать, подменять, хранить, сериализовать.
Реализация на Python
Python — динамический язык, поэтому здесь есть несколько способов реализации. Начнём с классического объектно-ориентированного подхода.
Классическая ООП-реализация
from abc import ABC, abstractmethod
class PaymentStrategy(ABC):
"""Абстрактная стратегия оплаты."""
@abstractmethod
def pay(self, amount: float) -> None:
pass
class CardPayment(PaymentStrategy):
def __init__(self, card_number: str):
self._card_number = card_number
def pay(self, amount: float) -> None:
print(f"Списано {amount} с карты {self._card_number[-4:]}")
class PayPalPayment(PaymentStrategy):
def __init__(self, email: str):
self._email = email
def pay(self, amount: float) -> None:
print(f"Оплата через PayPal ({self._email}): {amount}")
class CryptoPayment(PaymentStrategy):
def __init__(self, wallet_address: str):
self._wallet = wallet_address
def pay(self, amount: float) -> None:
print(f"Транзакция на кошелёк {self._wallet[:8]}...: {amount} BTC")
class PaymentContext:
"""Контекст — не знает, какой именно способ оплаты используется."""
def __init__(self, strategy: PaymentStrategy):
self._strategy = strategy
def set_strategy(self, strategy: PaymentStrategy) -> None:
"""Стратегию можно менять в рантайме."""
self._strategy = strategy
def checkout(self, amount: float) -> None:
self._strategy.pay(amount)
# Использование
payment = PaymentContext(CardPayment("4111111111111234"))
payment.checkout(1500.00)
payment.set_strategy(PayPalPayment("user@example.com"))
payment.checkout(750.00)
Обратите внимание на метод set_strategy — это одна из ключевых особенностей паттерна: стратегию можно менять в процессе работы программы, не пересоздавая контекст.
Функциональный подход в Python
В Python стратегии не обязательно оформлять как классы — функции являются объектами первого класса, что даёт более лаконичный вариант:
from typing import Callable
def card_payment(amount: float) -> None:
print(f"Оплата картой: {amount}")
def paypal_payment(amount: float) -> None:
print(f"Оплата через PayPal: {amount}")
class PaymentContext:
def __init__(self, strategy: Callable[[float], None]):
self._strategy = strategy
def checkout(self, amount: float) -> None:
self._strategy(amount)
payment = PaymentContext(card_payment)
payment.checkout(500.0)
payment._strategy = paypal_payment
payment.checkout(300.0)
Этот подход работает, но у него есть ограничение: функции не несут состояния. Если стратегии нужны параметры (номер карты, адрес кошелька), придётся использовать замыкания или functools.partial — что снижает читаемость. Классовый подход здесь нагляднее.
Реализация на Java
Java — строготипизированный язык, и паттерн Strategy здесь выглядит особенно органично, поскольку интерфейсы — основной инструмент абстракции.
Через интерфейс
// Интерфейс стратегии
public interface SortStrategy {
void sort(int[] array);
}
// Конкретные стратегии
public class BubbleSort implements SortStrategy {
@Override
public void sort(int[] array) {
System.out.println("Сортировка пузырьком");
// реализация bubble sort
for (int i = 0; i < array.length - 1; i++) {
for (int j = 0; j < array.length - i - 1; j++) {
if (array[j] > array[j + 1]) {
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
}
}
public class QuickSort implements SortStrategy {
@Override
public void sort(int[] array) {
System.out.println("Быстрая сортировка");
quickSort(array, 0, array.length - 1);
}
private void quickSort(int[] arr, int low, int high) {
if (low < high) {
int pi = partition(arr, low, high);
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}
private int partition(int[] arr, int low, int high) {
int pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; j++) {
if (arr[j] <= pivot) {
i++;
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;
return i + 1;
}
}
// Контекст
public class Sorter {
private SortStrategy strategy;
public Sorter(SortStrategy strategy) {
this.strategy = strategy;
}
public void setStrategy(SortStrategy strategy) {
this.strategy = strategy;
}
public void sort(int[] array) {
strategy.sort(array);
}
}
// Использование
public class Main {
public static void main(String[] args) {
int[] data = {64, 34, 25, 12, 22, 11, 90};
Sorter sorter = new Sorter(new BubbleSort());
sorter.sort(data);
// Переключаемся на быструю сортировку без изменения контекста
sorter.setStrategy(new QuickSort());
sorter.sort(data);
}
}
Лямбды в Java 8+
Начиная с Java 8, функциональные интерфейсы и лямбды позволяют писать стратегии компактнее:
import java.util.Arrays;
import java.util.function.Consumer;
public class Sorter {
private Consumer<int[]> strategy;
public Sorter(Consumer<int[]> strategy) {
this.strategy = strategy;
}
public void sort(int[] array) {
strategy.accept(array);
}
}
// Использование с лямбдой
Sorter sorter = new Sorter(arr -> Arrays.sort(arr));
sorter.sort(new int[]{5, 3, 1, 4, 2});
Это удобно для простых случаев, но если стратегия сложная и требует нескольких методов или состояния — полноценный класс с интерфейсом предпочтительнее.
Strategy vs. простой полиморфизм: в чём разница?
Это, пожалуй, самый важный вопрос — и самый частый источник путаницы. Многие разработчики, увидев паттерн Strategy впервые, говорят: «Но это же просто полиморфизм». И они правы лишь отчасти.
Что общего
И Strategy, и обычный полиморфизм используют интерфейсы и подстановку реализаций. Оба позволяют вызывать pay() или sort(), не зная конкретного типа объекта.
Ключевые отличия
1. Намерение и семантика
Полиморфизм — это про «что это такое» (иерархия типов). Strategy — про «как это делается» (выбор алгоритма). Dog и Cat — разные типы, которые по-разному реализуют makeSound(). Это полиморфизм. BubbleSort и QuickSort — не разные типы сортировщиков, это разные способы сортировки одного и того же. Это Strategy.
2. Изменяемость в рантайме
При обычном полиморфизме объект создаётся как конкретный тип и остаётся им. Strategy позволяет подменить алгоритм у существующего объекта через setStrategy() — без пересоздания контекста.
3. Отделение алгоритма от данных
В полиморфизме поведение неотделимо от объекта. В Strategy алгоритм — это самостоятельный объект, который можно передавать, хранить в конфигурации, выбирать по условию извне.
# Полиморфизм: поведение жёстко привязано к типу
class Duck:
def quack(self):
print("Кря!")
class RubberDuck(Duck):
def quack(self):
print("Пи-пи!")
# Strategy: поведение — отдельный объект
class Duck:
def __init__(self, quack_strategy):
self._quack = quack_strategy
def quack(self):
self._quack()
real_quack = lambda: print("Кря!")
rubber_quack = lambda: print("Пи-пи!")
duck = Duck(real_quack)
duck.quack() # Кря!
duck._quack = rubber_quack # Меняем поведение без создания нового объекта
duck.quack() # Пи-пи!
Именно эта гибкость делает Strategy мощным инструментом там, где полиморфизм недостаточен.
Когда применять Strategy, а когда — нет
Паттерн хорошо подходит, если:
- Есть несколько вариантов алгоритма, которые нужно переключать в рантайме или конфигурировать извне.
- Логика выбора алгоритма засоряет основной класс (
if method == "X",if mode == "Y"). - Алгоритмы нужно тестировать независимо друг от друга.
- Планируется добавление новых вариантов без изменения существующего кода.
Паттерн избыточен, если:
- Вариантов поведения всего два, и они никогда не изменятся.
- Алгоритм не имеет состояния и не будет меняться — достаточно простой функции.
- Команда небольшая, а код одноразовый — дополнительная абстракция только усложнит чтение.
Как и любой паттерн, Strategy — это инструмент, а не догма. Применять его ради «правильной архитектуры» без реальной необходимости — это оверинжиниринг, который вредит проекту не меньше, чем гигантский if-else.
Если хотите систематически разобраться с тем, когда и как применять паттерны проектирования в реальных проектах, стоит обратить внимание на курс «» — там эти темы разбираются на практических задачах, а не в отрыве от реального кода.
Типичные ошибки при реализации
1. Контекст знает о конкретных стратегиях
# Плохо: контекст проверяет тип стратегии
def checkout(self, amount):
if isinstance(self._strategy, CryptoPayment):
amount = self._convert_to_btc(amount)
self._strategy.pay(amount)
Если контекст начинает различать стратегии — паттерн реализован неправильно. Конвертация должна быть внутри CryptoPayment.pay().
2. Стратегия знает о контексте
Стратегия должна быть независимой. Если она обращается к полям контекста напрямую — это нарушение инкапсуляции и признак плохого разделения ответственности.
3. Слишком тонкозернистые стратегии
Выносить в стратегию одну строку кода — бессмысленно. Стратегия оправдана, когда алгоритм достаточно сложен или вариативен, чтобы жить отдельно.
4. Игнорирование фабрики для создания стратегий
Если выбор стратегии всё равно происходит через if-else где-то в клиентском коде — проблема не исчезла, она просто переехала. Для выбора стратегии по параметру стоит использовать словарь-маппинг или фабричный метод:
PAYMENT_STRATEGIES = {
"card": CardPayment,
"paypal": PayPalPayment,
"crypto": CryptoPayment,
}
def get_strategy(method: str, **kwargs) -> PaymentStrategy:
cls = PAYMENT_STRATEGIES.get(method)
if not cls:
raise ValueError(f"Неизвестный метод оплаты: {method}")
return cls(**kwargs)
Теперь добавление нового способа оплаты — это создание нового класса и одна строка в словаре. Существующий код не трогается.
Вывод
Паттерн Strategy — это не про синтаксис и не про «красивый код ради красоты». Это про управление изменениями. Когда требования меняются (а они всегда меняются), код, построенный на Strategy, расширяется добавлением нового класса. Код с гигантским if-else — модификацией существующего, с риском регрессий.
Разница между Strategy и простым полиморфизмом — в намерении: полиморфизм описывает иерархию типов, Strategy инкапсулирует изменяемое поведение как самостоятельный объект. Это тонкое, но принципиальное различие, которое становится очевидным в реальных проектах.
Python и Java реализуют паттерн по-разному в деталях, но идея одна: отделите то, что меняется, от того, что остаётся стабильным. Именно этот принцип лежит в основе большинства паттернов GoF — и именно его понимание отличает архитектурно грамотный код от набора работающих, но хрупких конструкций.
Если хотите выстроить системное понимание паттернов проектирования и научиться применять их осознанно — курс «» может стать хорошей отправной точкой для углублённого изучения темы.
Комментарии
Пока нет комментариев