© 2025
Александр Легалов
Содержание
Стратегия (Strategy)
Почему Стратегия идет вслед за декоратором? Потому что данный образец упоминается при обсуждении декоратора. Стратегия определяет семейство алгоритмов, инкапсулирует каждый из них и делает
их взаимозаменяемыми. Позволяет изменять алгоритмы независимо от клиентов, которые ими пользуются. Основные условия для применения паттерна:
- позволяет настроить класс одним из многих возможных вариантов поведения;
- наличие нескольких разновидностей алгоритмов для решения одной и той же задачи, обеспечивающих различные критерии качества (возможны иерархические комбинации алгоритмов);
- в алгоритме содержатся данные, которые должны быть скрыты от клиента;
- в классе определено много вариантов поведения, представленных условными операторами, которые проще перенести в отдельные классы стратегий.
Графическое представление ОО структуры Стратегии приведено на рисунке.
Структура паттерна Стратегия
Объектно-ориентированная реализация Стратегии
В целом образец является достаточно простым. В частности клиент может быть любым программным объектом, включая обычную функцию, содержащую указатель на обобщенную стратегия, представленную базовым классом. Особенностью конкретных стратегий является наличие интерфейсов, содержащих методы с одинаковой сигнатурой. Сокрытые за общим методом алгоритмы могут быть произвольными. Для простоты и следуя ориентации на животных используем в качестве алгоритмов те же методы, что использовались в декораторе для задания имени и возраста животных. В данном случае даже не так важно, какое из животных имеется в виду, так как указатель на стратегию будет расположен в базовом классе Animal . В качестве альтернативных методов, реализующих пару стратегий, будем выводить каждый раз кличку животного и возраст, но в разных последовательностях. Сами животные в ОО программе представлены следующим кодом.
// Обобщенное животное, определяемое в базовом классе.
// Также используется для обобщение декораторов.
class Animal {
Strategy* strategy;
std::string name;
int age;
public:
Animal(std::string n, int a): name{n}, age{a}, strategy{nullptr} {}
virtual ~Animal() {}
virtual void GetInfo() const = 0;
void SetStrategy(Strategy* s) {strategy = s;}
void UseStrategy() {
GetInfo();
if(strategy != nullptr) {
std::cout << ". ";
strategy->Action(name, age);
}
std::cout << "\n";
}
};
// Утка
class Duck : public Animal {
public:
Duck(std::string n, int a): Animal{n, a} {}
void GetInfo() const override {
std::cout << "I am the Duck";
}
};
// Собака
class Dog : public Animal {
public:
Dog(std::string n, int a): Animal{n, a} {}
void GetInfo() const override {
std::cout << "I am the Dog";
}
};
Его отличие от предшествующих вариантов только в дополнительных полях, задающих связь с подключаемой стратегией, а также фиксирующих кличку и возраст. Так как стратегия может изменяться в процессе выполнения, то для этого реализован метод SetStrategy . Любое животное выступает в роли клиента, пользующегося различными стратегиями. Для этого используется метод UseStrategy .
Сами стратегии формируются от общего базового класса и реализуют различные алгоритмы через расширяемые виртуальные методы, которые предоставляют одинаковую сигнатуру для альтернативных операций. В данном случае интерфейс содержит только один метод Action , обеспечивающий вывод информации о животном в альтернативных последовательностях.
// Абстрактный класс, определяющий обобщенную стратегию
class Strategy {
public:
virtual ~Strategy() {}
virtual void Action(std::string& name, int age) = 0;
};
// Конкретная стратегия, осуществляющая вывод в начале имени, а затем возраста
class NameAgeStrategy : public Strategy {
public:
void Action(std::string& name, int age) {
std::cout << "Name is " << name << ", age = " << age;
}
};
// Конкретная стратегия, осуществляющая вывод в начале возраста, а затем имени
class AgeNameStrategy : public Strategy {
public:
void Action(std::string& name, int age) {
std::cout << "Age = " << age << ", name is " << name;
}
};
Клиент используется в тестовой функции и осуществляет только получение указателя на базовый класс Animal , через который подключаются конкретные животные, использующие разные стратегии.
// Клиент запускает стратегию, подключенную к животному
void ClientCode(Animal* a) {
a->UseStrategy();
}
int main() {
NameAgeStrategy nas;
AgeNameStrategy ans;
Duck duck("Grey Neck", 2);
duck.SetStrategy(&nas);
ClientCode(&duck);
duck.SetStrategy(&ans);
ClientCode(&duck);
Dog dog("Rex", 5);
dog.SetStrategy(&nas);
ClientCode(&dog);
dog.SetStrategy(&ans);
ClientCode(&dog);
return 0;
}
Процедурно-параметрические отношения в Стратегии
Объяснение того, что Стратегия используется также и для замены процедурной реализации, состоящей из явных условных проверок признаков, и не более того, приведено в одном из условий применения паттерна. Проблема процедурного подхода заключается в необходимости изменять написанный код при добавлении очередного алгоритма. Однако сама схема данного образца весьма простая
Процедурное представление аналога Стратегии
Процедурно-параметрическая имитация Стратегии
Процедурно-параметрическая версия программы непосредственно реализует представленную схему, избегая при этом недостатков чистого процедурного подхода. Стратегия по сути является эволюционно расширяемым перечислимым типом, используемым вместо интерфейсных классов ОО версии. Любые функции, определяющие альтернативные стратегии, как и новые стратегии могут добавляться независимо и эволюционно. Для представленного примера обобщенная стратегия, ее специализации и параметрическая функция Action выглядят следующим образом:
// Обобщенная стратегия
typedef struct Strategy {}<> Strategy;
// Обобщающая функция, определяющая общую сигнатуру для алгоритмов
void Action<Strategy* s>(char* name, int age) = 0;
// Конкретные стратегии задаются перечислимыми типом
Strategy + <NameAge: void;>;
Strategy + <AgeName: void;>;
// Обработчики специализаций, определяемые конкрентным стратегиями.
void Action<Strategy.NameAge* s>(char* name, int age) {
printf("Name is %s, age = %d", name, age);
}
void Action<Strategy.AgeName* s>(char* name, int age) {
printf("Age = %d, name is %s", age, name);
}
Обобщение животных содержит, как и в ОО программе, указатель на подключаемую стратегию, кличку и возраст. А также использует дополнительные функции для инициализации, смены стратегии и запуска нужной альтернативной стратегии:
// Обобщенное животное
typedef struct Animal {
Strategy* strategy;
char* name;
int age;
}<> Animal;
void GetInfo<Animal* a>() = 0;
// Начальная инициализация вместо отсутствующего конструктора.
void InitAnimal(Animal* a, char* name, int age) {
a->strategy = NULL;
a->name = name;
a->age = age;
}
// Установка стратегии для любого животного
void SetStrategy(Animal* a, Strategy* s) {a->strategy = s;}
// Использование стратегии животным
void UseStrategy(Animal* a) {
GetInfo<a>();
if(a->strategy != NULL) {
printf(". ");
Action<a->strategy>(a->name, a-> age);
}
printf("\n");
}
// Утка
typedef struct Duck {} Duck;
Animal + <Duck;>;
void GetInfo<Animal.Duck* a>() {
printf("I am the Duck");
};
// Собака
typedef struct Dog {} Dog;
Animal + <Dog;>;
void GetInfo<Animal.Dog* a>() {
printf("I am the Dog");
};
Внешний клиент и тестовая функция реализованы аналогично ОО коду.
// Клиент запускает сформированных животных
void ClientCode(Animal* a) {
UseStrategy(a);
}
int main() {
struct Strategy.NameAge nas;
struct Strategy.AgeName ans;
struct Animal.Duck duck;
InitAnimal((Animal*)&duck, "Grey Neck", 2);
SetStrategy((Animal*)&duck, (Strategy*)&nas);
ClientCode((Animal*)&duck);
SetStrategy((Animal*)&duck, (Strategy*)&ans);
ClientCode((Animal*)&duck);
struct Animal.Dog dog;
InitAnimal((Animal*)&dog, "Rex", 5);
SetStrategy((Animal*)&dog, (Strategy*)&nas);
ClientCode((Animal*)&dog);
SetStrategy((Animal*)&dog, (Strategy*)&ans);
ClientCode((Animal*)&dog);
return 0;
}
Стратегия или Декоратор?
Сравнение стратегии и декоратора обсуждается в книге GoF [gof-patternsГамма Э., Хелм Р., Джонсон Р., Влиссидес Дж. Паттерны объектно-ориентированного проектирования. - СПб.: Питер, 2020. - 448 с.]. Сопоставляются их более сложные схемы, когда стратегия может содержать дополнительные вложенные стратегии~\ref{decorator-vs-strategy}
Сопоставление Декоратора и Стратегии
Более универсальные схемы хорошо реализуются с использованием ОО подхода. Необходимо только добавить в базовый класс стратегии указатель на другую стратегию и сделать в альтернативных методах или в методах базового класса необходимый переход на стратегию, которая связана с предшествующей стратегией.
// Абстрактный класс, определяющий обобщенную стратегию
class Strategy {
protected:
Strategy* nextStrategy;
public:
Strategy(): nextStrategy{nullptr} {}
void SetStrategy(Strategy* s) {nextStrategy = s;}
virtual ~Strategy() {}
virtual void Action(std::string& name, int age) {
if(nextStrategy != nullptr) {
std::cout << "\n ";
nextStrategy->Action(name, age);
}
}
};
// Конкретная стратегия, осуществляющая вывод в начале имени, а затем возраста
class NameAgeStrategy : public Strategy {
public:
void Action(std::string& name, int age) {
std::cout << "Name is " << name << ", age = " << age;
// Необязательное действие, если алгоритму не нужно
Strategy::Action(name, age);
}
};
// Конкретная стратегия, осуществляющая вывод в начале возраста, а затем имени
class AgeNameStrategy : public Strategy {
public:
void Action(std::string& name, int age) {
std::cout << "Age = " << age << ", name is " << name;
// Необязательное действие, если алгоритму не нужно
Strategy::Action(name, age);
}
};
Код клиента остается прежним, а в тестовой функции необходимо поменять имена декораторов:
...
Duck newDuck("Black Cape", 4);
newDuck.SetStrategy(&nas);
nas.SetStrategy(&ans);
ClientCode(&newDuck);
newDuck.SetStrategy(&ans);
ClientCode(&newDuck);
...
Учитывая специфику более продвинутой реализации, можно слегка поменять схему, описывающую отношения между данными, определяющими стратегию, и функциями, осуществляющую полиморфную обработку.
Графическое представление ОО паттерна <<Стратегия>> при использовании вложенных подстратегий
Нет никаких проблем реализовать имитацию подобной схемы с использованием ПП подхода. Отличительной чертой является то, вместо изменения базового класса, используемого в ОО подходе, передачу указателя на обобщение нужно сделать в отдельной функции \verb|NextAction|. Это связано со спецификой передачи полиморфного параметра, признак которого не соответствует признаку обобщения. Но в целом схема аналогична по решению и работает в различных схожих ситуациях, когда в обработчиках специализаций нужно использовать передачу через обобщенный параметр. Стратегии имитируются следующим образом:
// Обобщенная стратегия
typedef struct Strategy {struct Strategy* nextStrategy;}<> Strategy;
// Обобщающая функция, определяющая общую сигнатуру для алгоритмов
void Action<Strategy* s>(char* name, int age) = 0;
// Начальная инициализация стратегии
void InitStrategy(Strategy* s) {s->nextStrategy = NULL;}
// Установка стратегии внутри стратегии
void SetNextStrategy(Strategy* s, Strategy* ns) {s->nextStrategy = ns;}
// Функция, осуществляющая переадресацию на дополнительную стратегию
void NextAction(Strategy* s, char* name, int age) {
if((Strategy*)s->nextStrategy != NULL) {
printf("\n ");
Action<s->nextStrategy>(name, age);
}
}
// Конкретные стратегии задаются перечислимыми типом
Strategy + <NameAge: void;>;
Strategy + <AgeName: void;>;
// Обработчики специализаций, определяемые конкрентным стратегиями.
void Action<Strategy.NameAge* s>(char* name, int age) {
printf("Name is %s, age = %d", name, age);
// Необязательное действие, если алгоритму не нужно
NextAction((Strategy*)s, name, age);
}
void Action<Strategy.AgeName* s>(char* name, int age) {
printf("Age = %d, name is %s", age, name);
// Необязательное действие, если алгоритму не нужно
NextAction((Strategy*)s, name, age);
}
В тестовом коде также появляется следующий дополнительный фрагмент:
...
struct Animal.Duck newDuck;
InitAnimal((Animal*)&newDuck, "Black Cape", 4);
SetStrategy((Animal*)&newDuck, (Strategy*)&nas);
SetNextStrategy((Strategy*)&nas, (Strategy*)&ans);
ClientCode((Animal*)&newDuck);
SetStrategy((Animal*)&newDuck, (Strategy*)&ans);
ClientCode((Animal*)&newDuck);
...
Содержание
|