Перейти к основному содержимому

Паттерн "Стратегия" в C# — когда нужен, а когда достаточно делегата

Разработчику Архитектору

Обзор поведенческих паттернов — в главе о Strategy и соседях. Здесь — практика на C#: как не раздувать проект лишними классами и когда интерфейс по-прежнему уместен.

Загрузка редактора схем…

Суть паттерна

Стратегия инкапсулирует семейство алгоритмов и позволяет подменять их во время выполнения. Контекст делегирует работу объекту "стратегии" и не зависит от конкретной реализации.

Типичные задачи:

  • выбор способа сортировки или сжатия;
  • расчёт скидки, комиссии, налога;
  • маршрутизация платежа по региону или каналу.

Паттерн убирает длинные if / switch и упрощает тесты: алгоритм подставляется извне.

Загрузка ArchiStyler…

Классическая реализация (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
Внутреннее состояниеКэш токена, настройки провайдера, счётчики
Регистрация в DIIPaymentStrategy → реализации по региону, моки в тестах
Именованная реализация в библиотекеПубличный 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.


См. также

См. также

Другие статьи этого же раздела в боковом меню (как на странице "О разделе").