Паттерн "Стратегия" в C# — когда нужен, а когда достаточно делегата
Обзор поведенческих паттернов — в главе о Strategy и соседях. Здесь — практика на C#: как не раздувать проект лишними классами и когда интерфейс по-прежнему уместен.
Суть паттерна
Стратегия инкапсулирует семейство алгоритмов и позволяет подменять их во время выполнения. Контекст делегирует работу объекту "стратегии" и не зависит от конкретной реализации.
Типичные задачи:
- выбор способа сортировки или сжатия;
- расчёт скидки, комиссии, налога;
- маршрутизация платежа по региону или каналу.
Паттерн убирает длинные if / switch и упрощает тесты: алгоритм подставляется извне.
Классическая реализация (GoF)
Стандартный каркас — интерфейс, несколько классов-реализаций и контекст, который хранит ссылку на стратегию:
public interface ISortStrategy
{
void Sort(List<int> list);
}
public class BubbleSortStrategy : ISortStrategy
{
public void Sort(List<int> list)
{
// пузырьковая сортировка
}
}
public class QuickSortStrategy : ISortStrategy
{
public void Sort(List<int> list)
{
// быстрая сортировка
}
}
public class SortContext
{
private ISortStrategy _strategy;
public SortContext(ISortStrategy strategy) => _strategy = strategy;
public void SetStrategy(ISortStrategy strategy) => _strategy = strategy;
public void ExecuteSort(List<int> list) => _strategy.Sort(list);
}
Для каждого нового алгоритма появляется отдельный класс (часто отдельный файл). Контекст знает только ISortStrategy. Так устроены учебники и многие корпоративные шаблоны.
Для простой подмены одной операции такой объём часто избыточен: пять классов ради замены одной функции усложняют навигацию по репозиторию без выигрыша в гибкости.
Идиоматичный C# — Func, Action и лямбды
С .NET 3.5 в языке есть делегаты и лямбды. Стратегия в терминах проектирования — это "передать поведение параметром"; в C# функции уже объекты первого класса.
Тот же контекст без интерфейсов:
public class SortContext
{
public void ExecuteSort(List<int> list, Action<List<int>> sortAlgorithm)
{
sortAlgorithm(list);
}
}
var context = new SortContext();
context.ExecuteSort(numbers, list => list.Sort());
context.ExecuteSort(numbers, list =>
{
// своя логика сортировки
});
Один метод принимает 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
См. также
Другие статьи этого же раздела в боковом меню (как на странице "О разделе"). Паттерн — это повторяющийся шаблон, узор или схема. Паттерны встречаются повсюду — в природе, архитектуре, поведении людей и, конечно, в программировании. Порождающие паттерны проектирования — это группа шаблонов, направленных на решение задач, связанных с созданием объектов. Структурные паттерны — это группа шаблонов проектирования, решающих задачи организации классов и объектов таким образом, чтобы обеспечить гибкую архитектуру программного обеспечения. Поведенческие паттерны — это группа шаблонов проектирования, которые определяют способы взаимодействия объектов и распределения ответственности между ними. Архитектурные паттерны — это проверенные решения для организации структуры программного обеспечения. Интеграция систем — одна из центральных задач в современной разработке программного обеспечения. Паттерны доменного моделирования представляют собой проверенные решения для организации бизнес-логики в программных системах. Паттерн Iterator в C# — ручной IEnumerator, генерация итератора компилятором через yield return, ленивость, LINQ и случаи, когда класс писать всё же нужно. Abstract Factory в C# и .NET — классическая схема через интерфейсы, замена через DI-контейнер, фабричный делегат и keyed services в .NET 8. Паттерн Command в C# — классическая схема, делегаты, MediatR, очередь задач, undo и критерии выбора между объектом команды и простым вызовом сервиса. Паттерн Observer в C# — event и делегаты, IObservable IObserver, слабая связанность, отписка и как не поймать утечки памяти в долгоживущих сервисах.Обзор паттернов проектирования
Порождающие паттерны
Структурные паттерны
Поведенческие паттерны
Архитектурные паттерны
Паттерны интеграции внешних систем
Паттерны проектирования доменных моделей
Итератор в C#
Фабрика в C#
Команда в C#
Наблюдатель в C#