Перейти к содержанию

Strategy (Стратегия)

Категория: паттерн поведения.

Проблема

Есть задача, которую можно решить несколькими взаимозаменяемыми алгоритмами (расчёт скидки, сортировка, сжатие, валидация), и выбор конкретного алгоритма должен происходить в рантайме, а не быть жёстко зашит в код. Наивное решение - один метод с большим if/switch по типу алгоритма - работает, но: - при добавлении нового варианта приходится модифицировать существующий метод (нарушение принципа открытости/закрытости); - логика разных алгоритмов перемешивается в одном месте, что усложняет чтение и тестирование каждого варианта по отдельности.

Решение

  1. Для семейства алгоритмов выделяется общий интерфейс с одним (реже - несколькими) методом.
  2. Каждый конкретный алгоритм оформляется отдельным классом, реализующим этот интерфейс.
  3. Класс-контекст, которому нужен алгоритм, хранит ссылку на интерфейс стратегии (а не на конкретную реализацию) и делегирует ей выполнение операции.
  4. Конкретная реализация подставляется в контекст снаружи - через конструктор, свойство или параметр метода.

Структура

  • Strategy - общий интерфейс алгоритма.
  • ConcreteStrategyA/B/... - конкретные реализации алгоритма.
  • Context - хранит ссылку на Strategy и делегирует ей выполнение операции, не зная, какая именно реализация подставлена.

Когда применять

  • Нужно выбирать между несколькими вариантами одного и того же поведения в рантайме.
  • Метод содержит разрастающийся if/switch, различающий поведение по типу/режиму.
  • Нужно изолировать детали конкретного алгоритма от кода, который им пользуется, чтобы упростить тестирование каждого варианта по отдельности.

Плюсы

  • Новый алгоритм добавляется новым классом, без изменения существующего кода контекста (открытость/закрытость).
  • Каждую стратегию можно тестировать в изоляции.
  • Убирает разрастающиеся условные конструкции по типу поведения.

Минусы

  • Клиентский код должен знать о различиях между стратегиями, чтобы выбрать подходящую - паттерн не решает вопрос "как выбрать стратегию", он лишь упрощает её подстановку.
  • Для очень простых, не имеющих состояния алгоритмов создание отдельного интерфейса и класса может быть избыточным.

Интерфейс vs делегат

В C# для стратегий без собственного состояния и с одной операцией часто разумнее использовать не интерфейс с одним методом, а делегат (Func<T, TResult>, Comparison<T>, Predicate<T>) - это тот же паттерн Strategy, но выраженный через встроенный в язык механизм функций как объектов первого класса, без лишней иерархии классов. Интерфейс имеет смысл заводить, когда: - у стратегии несколько связанных операций; - реализации нужно хранить собственное состояние или зависимости, которые удобнее внедрять через конструктор класса, а не через замыкание.

Отличие от Template Method

Strategy заменяет весь алгоритм целиком через композицию (контекст содержит ссылку на объект-стратегию). Template Method фиксирует скелет алгоритма в базовом классе и позволяет переопределить только отдельные шаги через наследование. Strategy гибче (можно менять поведение в рантайме, без наследования), Template Method - проще для случаев, когда общей логики между вариантами больше, чем различий.

Пример в .NET Framework / BCL

  • IComparer<T> и делегат Comparison<T>, используемые в List<T>.Sort(), Array.Sort().
  • IEqualityComparer<T> в словарях и HashSet<T>.
  • Middleware-компоненты авторизации (IAuthorizationHandler) в ASP.NET Core - разные стратегии проверки прав доступа.

Пример реализации на C#

Strategy.cs
using System;
using System.Collections.Generic;
using System.Linq;

namespace DesignPatterns.Behavioral.Strategy
{
    // Вариант 1: классическая реализация через интерфейс.
    // Уместна, когда у алгоритма несколько операций или своё внутреннее состояние.
    public interface IDiscountStrategy
    {
        decimal Apply(decimal totalPrice);
    }

    public sealed class NoDiscount : IDiscountStrategy
    {
        public decimal Apply(decimal totalPrice) => totalPrice;
    }

    public sealed class PercentageDiscount : IDiscountStrategy
    {
        private readonly decimal _percent;

        public PercentageDiscount(decimal percent) => _percent = percent;

        public decimal Apply(decimal totalPrice) => totalPrice * (1 - _percent / 100m);
    }

    public sealed class FixedAmountDiscount : IDiscountStrategy
    {
        private readonly decimal _amount;

        public FixedAmountDiscount(decimal amount) => _amount = amount;

        public decimal Apply(decimal totalPrice) => Math.Max(0, totalPrice - _amount);
    }

    // Контекст, использующий стратегию. Он не знает, какая конкретно скидка применяется -
    // это решает вызывающий код, подставляя нужную реализацию.
    public sealed class Order
    {
        private readonly IDiscountStrategy _discountStrategy;
        private readonly decimal _totalPrice;

        public Order(decimal totalPrice, IDiscountStrategy discountStrategy)
        {
            _totalPrice = totalPrice;
            _discountStrategy = discountStrategy;
        }

        public decimal GetFinalPrice() => _discountStrategy.Apply(_totalPrice);
    }

    // Вариант 2: стратегия как делегат - когда алгоритм это одна операция без состояния,
    // в C# зачастую нет смысла заводить под неё интерфейс и класс.
    public static class SortingDemo
    {
        public static IEnumerable<T> SortBy<T>(IEnumerable<T> items, Comparison<T> strategy)
        {
            var list = items.ToList();
            list.Sort(strategy);
            return list;
        }
    }

    public static class Demo
    {
        public static void Run()
        {
            var order1 = new Order(1000m, new PercentageDiscount(10));
            var order2 = new Order(1000m, new FixedAmountDiscount(150));

            Console.WriteLine($"Со скидкой 10%: {order1.GetFinalPrice():C}");
            Console.WriteLine($"Со скидкой 150: {order2.GetFinalPrice():C}");

            // Здесь стратегия сортировки передаётся просто как лямбда (делегат),
            // без отдельного интерфейса ISortStrategy.
            var names = new[] { "Борис", "Анна", "Вера" };
            var sorted = SortingDemo.SortBy(names, (a, b) => string.CompareOrdinal(a, b));
            Console.WriteLine(string.Join(", ", sorted));
        }
    }
}

Открыть Strategy.cs отдельно Скачать Strategy.cs