Паттерн "Стратегия" в C# — когда нужен, а когда достаточно делегата
Модель и масштаб — Основы БД, опорные темы, проектирование БД, пакетная работа. Карта — о разделе.
Обзор поведенческих паттернов — в главе о Strategy и соседях. Здесь — практика на C#: как не раздувать проект лишними классами и когда интерфейс по-прежнему уместен.
Play ITЗагрузка интерактивного демо…
Суть паттерна
Стратегия инкапсулирует семейство алгоритмов и позволяет подменять их во время выполнения. Контекст делегирует работу объекту "стратегии" и не зависит от конкретной реализации.
Типичные задачи:
- выбор способа сортировки или сжатия;
- расчёт скидки, комиссии, налога;
- маршрутизация платежа по региону или каналу.
Паттерн убирает длинные if / switch и упрощает тесты: алгоритм подставляется извне.
Play ITЗагрузка интерактивного демо…
Классическая реализация (GoF)
Стандартный каркас — интерфейс, несколько классов-реализаций и контекст, который хранит ссылку на стратегию:
Код ITЗагрузка примера кода…
Для каждого нового алгоритма появляется отдельный класс (часто отдельный файл). Контекст знает только ISortStrategy. Так устроены учебники и многие корпоративные шаблоны.
Для простой подмены одной операции такой объём часто избыточен: пять классов ради замены одной функции усложняют навигацию по репозиторию без выигрыша в гибкости.
Идиоматичный C# — Func, Action и лямбды
С .NET 3.5 в языке есть делегаты и лямбды. Стратегия в терминах проектирования — это "передать поведение параметром"; в C# функции уже объекты первого класса.
Тот же контекст без интерфейсов:
Код ITЗагрузка примера кода…
Один метод принимает Action<List<int>>. Поведение задаётся в месте вызова — без иерархии *Strategy.
Пример со скидкой:
public class PriceCalculator
{
public decimal Calculate(decimal price, Func<decimal, decimal> discountStrategy)
=> discountStrategy(price);
}
var calculator = new PriceCalculator();
decimal regular = calculator.Calculate(100m, price => price * 0.9m);
decimal vip = calculator.Calculate(100m, price => price * 0.8m);
Func<decimal, decimal> — контракт "цена на входе, цена на выходе". Это та же идея Strategy, выраженная средствами BCL.
Подробнее о типах делегатов — в главе про делегаты и события. В LINQ (OrderBy, Where, Select) стратегия сравнения и фильтрации тоже передаётся делегатом — см. LINQ в C#.
Когда интерфейс остаётся правильным выбором
Делегаты удобны, если стратегия — одна функция без состояния и без набора связанных операций.
Интерфейс уместен, когда:
| Признак | Почему интерфейс |
|---|---|
| Несколько связанных методов | Единый контракт, а не три разрозненных Func |
| Внутреннее состояние | Кэш токена, настройки провайдера, счётчики |
| Регистрация в DI | IPaymentStrategy → реализации по региону, моки в тестах |
| Именованная реализация в библиотеке | Публичный API, документация, версионирование |
Пример платёжной стратегии:
public interface IPaymentStrategy
{
bool Validate(PaymentDetails details);
PaymentResult Process(PaymentDetails details);
void Rollback(string transactionId);
}
Три метода образуют один сценарий. Разнести их на три делегата можно, но читаемость и тестирование через моки обычно страдают.
Типичная схема в ASP.NET Core — несколько реализаций IPaymentStrategy, выбор в рантайме по стране пользователя, регистрация в IServiceCollection. См. внедрение зависимостей.
Стратегия и "Состояние"
Оба паттерна выносят поведение в отдельные типы и меняют его динамически. Различие в источнике смены:
- Стратегия — клиент или контекст выбирает алгоритм снаружи; на время операции вариант обычно фиксирован.
- Состояние — объект сам переключает режим по внутренним правилам (черновик → на модерации → опубликован).
Подробнее — в поведенческих паттернах.
Чек-лист выбора формы
| Ситуация | Рекомендация |
|---|---|
| Одна операция, 2–3 варианта, логика на 1–5 строк | Func / Action или лямбда в вызове |
| Алгоритм сложный, много веток, нужны unit-тесты по классам | Класс + интерфейс |
| Несколько методов на один сценарий | Интерфейс |
| Подстановка через DI, смена в рантайме по конфигу | Интерфейс + регистрация в контейнере |
| Вариантов два, меняются редко | Достаточно switch или простого if — без паттерна |
Если после введения Strategy код сложнее объяснить за пять минут, форма реализации, скорее всего, тяжелее задачи.
Упростите до делегата или явной ветки.
Итог
Паттерн по-прежнему описывает полезное разделение "контекст / алгоритм". В C# для узкой подмены поведения чаще хватает Func<T> и Action<T>. Интерфейс и классы стратегий оправданы при богатом контракте, состоянии, DI и публичном расширяемом API.
Выбирайте форму под задачу, а не по привычке из учебника GoF.
См. также
- Поведенческие паттерны — обзор Strategy, Command, State
- Паттерн "Итератор" в C# —
yield returnи LINQ - Делегаты и лямбды
- Вопросы на собеседование .NET — Strategy