Алгоритмы

Strategy: подстановка алгоритмов во время выполнения

Введение

В этой статье разберём паттерн по существу, для случаев, где он РЕАЛЬНО оправдан: когда конкретный алгоритм для выполнения определённой задачи должен выбираться или изменяться во время выполнения, без изменения кода, использующего этот алгоритм.

Концепция

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

Пример кода

#include <iostream>
#include <memory>
#include <vector>
#include <algorithm>

class SortStrategy
{
public:
    virtual void sort(std::vector<int> &data) const = 0;
    virtual ~SortStrategy() = default;
};

class QuickSortStrategy : public SortStrategy // конкретная реализация — алгоритм 1
{
public:
    void sort(std::vector<int> &data) const override
    {
        std::sort(data.begin(), data.end()); // алгоритмического цикла — std::sort фактически quicksort-гибрид
        std::cout << "Отсортировано через QuickSort-подобный алгоритм" << std::endl;
    }
};

class StableSortStrategy : public SortStrategy // конкретная реализация — алгоритм 2, с другими гарантиями
{
public:
    void sort(std::vector<int> &data) const override
    {
        std::stable_sort(data.begin(), data.end()); // другая гарантия — устойчивая сортировка
        std::cout << "Отсортировано через устойчивую сортировку" << std::endl;
    }
};

class DataProcessor // Context — использует Strategy, НЕ зная конкретный алгоритм
{
public:
    explicit DataProcessor(std::unique_ptr<SortStrategy> strategy) : m_strategy(std::move(strategy)) {}

    void setStrategy(std::unique_ptr<SortStrategy> strategy) { m_strategy = std::move(strategy); } // ЗАМЕНА во время выполнения

    void processData(std::vector<int> &data)
    {
        m_strategy->sort(data); // делегирование — конкретный алгоритм определяется ТЕКУЩЕЙ стратегией
    }

private:
    std::unique_ptr<SortStrategy> m_strategy;
};

int main()
{
    std::vector<int> data = {5, 2, 8, 1, 9};

    DataProcessor processor(std::make_unique<QuickSortStrategy>());
    processor.processData(data);

    processor.setStrategy(std::make_unique<StableSortStrategy>()); // ИЗМЕНЕНИЕ стратегии во время выполнения
    processor.processData(data);

    return 0;
}
// Более лёгковесная альтернатива через std::function/лямбды 
// без полной иерархии классов, для более простых случаев
#include <functional>

class LightweightProcessor
{
public:
    explicit LightweightProcessor(std::function<void(std::vector<int>&)> strategy) : m_strategy(strategy) {}
    void process(std::vector<int> &data) { m_strategy(data); }

private:
    std::function<void(std::vector<int>&)> m_strategy;
};

// Использование с лямбдой — без необходимости в отдельном классе стратегии вовсе
LightweightProcessor processor([](std::vector<int> &data) { std::sort(data.begin(), data.end()); });

Пояснения к коду

DataProcessor::setStrategy(), заменяющий внутренний m_strategy новым объектом стратегии, показывает ключевую возможность паттерна — выбор конкретного алгоритма сортировки изменяется ВО ВРЕМЯ ВЫПОЛНЕНИЯ, без необходимости изменять сам код DataProcessor или создавать его новый экземпляр, что было бы невозможно столь же гибко выразить через простой if/switch внутри processData(), выбирающий между несколькими захардкоженными вариантами. Альтернативная реализация через std::function показывает современную, более лёгковесную альтернативу классической, основанной на иерархии классов реализации паттерна — для случаев, не требующих сложного состояния внутри самой стратегии, лямбда-выражение или обычный std::function достаточны и избегают избыточной инфраструктуры виртуальных классов там, где она не реально необходима.

Подводные камни

  • Применение полной, основанной на классах реализации Strategy там, где простой std::function/лямбда была бы достаточна — для случаев, где конкретная «стратегия» не нуждается в собственном, сложном внутреннем состоянии или нескольких связанных методах (только единственная операция), более лёгковесная альтернатива через std::function часто более уместна и менее многословна по сравнению с полной иерархией классов с виртуальными методами.
  • Игнорирование стоимости виртуальных вызовов для очень часто вызываемых, критичных по производительности стратегий — аналогично общему подводному камню паттернов с виртуальными вызовами, для горячих путей выполнения с очень высокой частотой вызова стратегии стоимость индирекции может стать заметным фактором, и для таких конкретных, измеренных случаев может быть оправдан переход к статическому полиморфизму через шаблоны (CRTP) вместо динамического выбора через виртуальные функции.
  • Применение Strategy для случая, где алгоритм реально никогда не изменяется в течение всего времени жизни программы (антипаттерн избыточного применения паттернов) — если выбор конкретного алгоритма реально известен и фиксирован уже на этапе компиляции или конфигурации запуска и никогда не изменяется динамически впоследствии, полная инфраструктура подменяемой во время выполнения стратегии избыточна, и более простое, прямое решение (обычная функция, или шаблонный параметр,для compile-time выбора) часто достаточно.
  • Отсутствие явной документации ожидаемого, согласованного контракта (предусловий, постусловий, инвариантов), который должна соблюдать КАЖДАЯ конкретная реализация стратегии — поскольку Context взаимодействует со стратегией исключительно через общий интерфейс, не зная деталей конкретной реализации, любое несоответствие реального поведения конкретной стратегии ожидаемому, общему контракту (например, неожиданные побочные эффекты или нарушенные гарантии, как стратегия, заявляющая о сортировке, но реально не до конца сортирующая данные) может привести к трудно диагностируемым ошибкам в коде, использующем Context, который полагается на общий, ожидаемый контракт, не подозревая о его нарушении конкретной, подставленной реализацией.