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

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

Стратегия — это поведенческий паттерн проектирования, который определяет семейство схожих алгоритмов и помещает каждый из них в собственный класс, после чего алгоритмы можно взаимозаменять прямо во время исполнения программы.

Паттерн Стратегия

Проблема

Вы решили написать приложение-навигатор для путешественников. Оно должно показывать красивую и удобную карту, позволяющую с лёгкостью ориентироваться в незнакомом городе.

Одной из самых востребованных функций являлся поиск и прокладывание маршрутов. Пребывая в неизвестном ему городе, пользователь должен иметь возможность указать начальную точку и пункт назначения, а навигатор — проложит оптимальный путь.

Первая версия вашего навигатора могла прокладывать маршрут лишь по дорогам, поэтому отлично подходила для путешествий на автомобиле. Но, очевидно, не все ездят в отпуск на машине. Поэтому следующим шагом вы добавили в навигатор прокладывание пеших маршрутов.

Через некоторое время выяснилось, что некоторые люди предпочитают ездить по городу на общественном транспорте. Поэтому вы добавили и такую опцию прокладывания пути.

Но и это ещё не всё. В ближайшей перспективе вы хотели бы добавить прокладывание маршрутов по велодорожкам. А в отдалённом будущем — интересные маршруты посещения достопримечательностей.

Код навигатора становится слишком раздутым
Код навигатора становится слишком раздутым.

Если с популярностью навигатора не было никаких проблем, то техническая часть вызывала вопросы и периодическую головную боль. С каждым новым алгоритмом код основного класса навигатора увеличивался вдвое. В таком большом классе стало довольно трудно ориентироваться.

Любое изменение алгоритмов поиска, будь то исправление багов или добавление нового алгоритма, затрагивало основной класс. Это повышало риск сделать ошибку, случайно задев остальной работающий код.

Кроме того, осложнялась командная работа с другими программистами, которых вы наняли после успешного релиза навигатора. Ваши изменения нередко затрагивали один и тот же код, создавая конфликты, которые требовали дополнительного времени на их разрешение.

Решение

Паттерн Стратегия предлагает определить семейство схожих алгоритмов, которые часто изменяются или расширяются, и вынести их в собственные классы, называемые стратегиями.

Вместо того, чтобы изначальный класс сам выполнял тот или иной алгоритм, он будет играть роль контекста, ссылаясь на одну из стратегий и делегируя ей выполнение работы. Чтобы сменить алгоритм, вам будет достаточно подставить в контекст другой объект-стратегию.

Важно, чтобы все стратегии имели общий интерфейс. Используя этот интерфейс, контекст будет независимым от конкретных классов стратегий. С другой стороны, вы сможете изменять и добавлять новые виды алгоритмов, не трогая код контекста.

Стратегии построения пути
Стратегии построения пути.

В нашем примере каждый алгоритм поиска пути переедет в свой собственный класс. В этих классах будет определён лишь один метод, принимающий в параметрах координаты начала и конца пути, а возвращающий массив точек маршрута.

Хотя каждый класс будет прокладывать маршрут по-своему, для навигатора это не будет иметь никакого значения, так как его работа заключается только в отрисовке маршрута. Навигатору достаточно подать в стратегию данные о начале и конце маршрута, чтобы получить массив точек маршрута в оговорённом формате.

Класс навигатора будет иметь метод для установки стратегии, позволяя изменять стратегию поиска пути на лету. Такой метод пригодится клиентскому коду навигатора, например, переключателям типов маршрутов в пользовательском интерфейсе.

Аналогия из жизни

Способы передвижения
Различные стратегии попадания в аэропорт.

Вам нужно добраться до аэропорта. Можно доехать на автобусе, такси или велосипеде. Здесь вид транспорта является стратегией. Вы выбираете конкретную стратегию в зависимости от контекста — наличия денег или времени до отлёта.

Структура

Структура классов паттерна Стратегия
  1. Контекст хранит ссылку на объект конкретной стратегии, работая с ним через общий интерфейс стратегий.

  2. Стратегия определяет интерфейс, общий для всех вариаций алгоритма. Контекст использует этот интерфейс для вызова алгоритма.

    Для контекста неважно, какая именно вариация алгоритма будет выбрана, так как все они имеют одинаковый интерфейс.

  3. Конкретные стратегии реализуют различные вариации алгоритма.

  4. Во время выполнения программы контекст получает вызовы от клиента и делегирует их объекту конкретной стратегии.

  5. Клиент должен создать объект конкретной стратегии и передать его в конструктор контекста. Кроме этого, клиент должен иметь возможность заменить стратегию на лету, используя сеттер. Благодаря этому, контекст не будет знать о том, какая именно стратегия сейчас выбрана.

Применимость

Когда вам нужно использовать разные вариации какого-то алгоритма внутри одного объекта.

Стратегия позволяет варьировать поведение объекта во время выполнения программы, подставляя в него различные объекты-поведения (например, отличающиеся балансом скорости и потребления ресурсов).

Когда у вас есть множество похожих классов, отличающихся только некоторым поведением.

Стратегия позволяет вынести отличающееся поведение в отдельную иерархию классов, а затем свести первоначальные классы к одному, сделав поведение этого класса настраиваемым.

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

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

Когда различные вариации алгоритмов реализованы в виде развесистого условного оператора. Каждая ветка такого оператора представляет собой вариацию алгоритма.

Стратегия помещает каждую лапу такого оператора в отдельный класс-стратегию. Затем контекст получает определённый объект-стратегию от клиента и делегирует ему работу. Если вдруг понадобится сменить алгоритм, в контекст можно подать другую стратегию.

Шаги реализации

  1. Определите алгоритм, который подвержен частым изменениям. Также подойдёт алгоритм, имеющий несколько вариаций, которые выбираются во время выполнения программы.

  2. Создайте интерфейс стратегий, описывающий этот алгоритм. Он должен быть общим для всех вариантов алгоритма.

  3. Поместите вариации алгоритма в собственные классы, которые реализуют этот интерфейс.

  4. В классе контекста создайте поле для хранения ссылки на текущий объект-стратегию, а также метод для её изменения. Убедитесь в том, что контекст работает с этим объектом только через общий интерфейс стратегий.

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

Преимущества и недостатки

  • Горячая замена алгоритмов на лету.

  • Изолирует код и данные алгоритмов от остальных классов.

  • Уход от наследования к делегированию.

  • Реализует принцип открытости/закрытости.

  • Усложняет программу за счёт дополнительных классов.

  • Клиент должен знать, в чём состоит разница между стратегиями, чтобы выбрать подходящую.

Пример 1

# include <iostream>
# include <memory>

using namespace std;

class Strategy
{
public:
        virtual ~Strategy() = default;

        virtual void algorithm() = 0;
};

class ConStrategy1 : public Strategy
{
public:
        void algorithm() override { cout << "Algorithm 1;" << endl; }
};

class ConStrategy2 : public Strategy
{
public:
        void algorithm() override { cout << "Algorithm 2;" << endl; }
};

class Context
{
protected:
        unique_ptr<Strategy> strategy;

public:
        explicit Context(unique_ptr<Strategy> ptr = make_unique<ConStrategy1>())
                : strategy(move(ptr)) {}
        virtual ~Context() = default;

        virtual void algorithmStrategy() = 0;
};

class Client1 : public Context
{
public:
        using Context::Context;

        void algorithmStrategy() override { strategy->algorithm(); }
};

int main()
{
        shared_ptr<Context> obj = make_shared<Client1>(make_unique<ConStrategy2>());

        obj->algorithmStrategy();
}

Пример 2

# include <iostream>
# include <memory>

using namespace std;

class Strategy
{
public:
        virtual ~Strategy() = default;

        virtual void algorithm() = 0;
};

class ConStrategy1 : public Strategy
{
public:
        void algorithm() override { cout << "Algorithm 1;" << endl; }
};

class ConStrategy2 : public Strategy
{
public:
        void algorithm() override { cout << "Algorithm 2;" << endl; }
};

class Context
{
public:
        virtual void algorithmStrategy(shared_ptr<Strategy> strategy) = 0;
};

class Client1 : public Context
{
public:
        void algorithmStrategy(shared_ptr<Strategy> strategy = make_shared<ConStrategy1>()) override
        {
                strategy->algorithm();
        }
};

int main()
{
        shared_ptr<Context> obj = make_shared<Client1>();
        shared_ptr<Strategy> strategy = make_shared<ConStrategy2>();
        
        obj->algorithmStrategy(strategy);
}

Пример на шаблоне

# include <iostream>
# include <memory>

using namespace std;

template <typename Type>
concept DefaultConstructible = is_default_constructible_v<Type>;

template <typename Type>
concept Algorithm = requires(Type t)
{
        t.algorithm();
};

class Strategy1
{
public:
        void algorithm() { cout << "Algorithm 1;" << endl; }
};

class Strategy2
{
public:
        void algorithm() { cout << "Algorithm 2;" << endl; }
};

template <Algorithm TStrategy = Strategy1>
requires DefaultConstructible<TStrategy>
class Context
{
private:
        unique_ptr<TStrategy> strategy{};

public:
        Context() : strategy(make_unique<TStrategy>()) {}

void algorithmStrategy() { strategy->algorithm(); }
};

int main()
{
        using Client = Context<Strategy2>;
        
        shared_ptr<Client> obj = make_shared<Client>();
        
        obj->algorithmStrategy();
}

Пример на шаблонном конструкторе

# include <iostream>
# include <memory>

using namespace std;

template <typename Derived, typename Base>
concept Derivative = is_abstract_v<Base> && is_base_of_v<Base, Derived>;

template <typename Type>
concept NotAbstract = !is_abstract_v<Type>;

template <typename Type>
concept DefaultConstructible = is_default_constructible_v<Type>;

template <typename Type>
concept Algorithm = requires(Type t)
{
        t.algorithm();
};

class Strategy
{
public:
        virtual ~Strategy() = default;

        virtual void algorithm() = 0;
};

class ConStrategy1 : public Strategy
{
public:
        void algorithm() override { cout << "Algorithm 1;" << endl; }
};

class ConStrategy2 : public Strategy
{
public:
        void algorithm() override { cout << "Algorithm 2;" << endl; }
};

template <typename T>
struct Type2Type
{
        using type = T;
};

class Context
{
private:
        unique_ptr<Strategy> strategy{};

public:
        template <Derivative<Strategy> TStrategy = ConStrategy1>
        requires NotAbstract<TStrategy> && Algorithm<TStrategy> && DefaultConstructible<TStrategy>
        Context(Type2Type<TStrategy>) : strategy(make_unique<TStrategy>()) {}

        void algorithmStrategy() { strategy->algorithm(); }
};

int main()
{
        shared_ptr<Context> obj = make_shared<Context>(Type2Type<ConStrategy2>());
        
        obj->algorithmStrategy();
}

Last updated