Стратегия инкапсулирует семейство алгоритмов и позволяет подменять их на лету. Звучит полезно. На практике это часто превращается в десятки файлов ради замены одной функции. В современном C# для этого есть средства попроще.
Классическая реализация
Стандартный подход выглядит так. Интерфейс
ISortStrategy, пять конкретных реализаций, класс-контекст, который принимает стратегию и вызывает её метод.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);
}
}
Для каждого нового алгоритма мы создаём отдельный класс. Контекст ничего не знает о конкретной реализации и работает через интерфейс. Всё по канону GoF.
Проблема в том, что для простых случаев это избыточно.
Что предлагает C# вместо этого
Начиная с .NET 3.5 в языке есть
Func<T>, Action<T> и лямбда-выражения. Strategy по сути означает «передай поведение как параметр». А в 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>>. Никаких интерфейсов, никаких отдельных файлов. Поведение передаётся напрямую.Ещё пример. Допустим, есть расчёт скидки:
public class PriceCalculator
{
public decimal Calculate(decimal price, Func<decimal, decimal> discountStrategy)
{
return discountStrategy(price);
}
}
var calculator = new PriceCalculator();
decimal result = calculator.Calculate(100m, price => price * 0.9m);
decimal vipResult = calculator.Calculate(100m, price => price * 0.8m);
Func<decimal, decimal> принимает цену, возвращает цену со скидкой. Стратегия задаётся в одну строку прямо в месте вызова.Когда интерфейсы всё-таки нужны
Делегаты хороши для простых случаев. Но если стратегия содержит несколько методов, внутреннее состояние или сложную логику, интерфейс остаётся правильным выбором.
public interface IPaymentStrategy
{
bool Validate(PaymentDetails details);
PaymentResult Process(PaymentDetails details);
void Rollback(string transactionId);
}
Здесь три связанных метода. Засунуть их в три отдельных
Func можно, но читаемость пострадает. Интерфейс даёт единую точку контракта и нормально тестируется через моки.Ещё один аргумент за интерфейс: когда реализации регистрируются в DI-контейнере и выбираются в рантайме. Например, разные провайдеры оплаты в зависимости от региона пользователя. Тут интерфейс упрощает конфигурацию и тестирование.
Если стратегия укладывается в одну функцию, используйте
Func<T> или Action<T>. Это проще, короче и не требует дополнительных абстракций. Если стратегия включает несколько связанных операций или вам нужна подмена реализаций через DI, интерфейс по-прежнему оправдан. Паттерн не устарел, но применять его стоит осознанно, а не по привычке.📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#il_люминатор