Паттерн Strategy
Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy — урок 95 из 104.
Паттерн Стратегия определяет семейство алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми. Это позволяет изменять поведение объекта во время выполнения без изменения его кода — алгоритм варьируется независимо от использующих его клиентов.
Паттерн состоит из трёх частей: интерфейса Стратегии, объявляющего метод алгоритма, Конкретных стратегий, реализующих различные варианты, и Контекста, который использует стратегию:
#include <iostream>
#include <memory>
// Интерфейс стратегии
class PaymentStrategy {
public:
virtual void pay(int amount) = 0;
virtual ~PaymentStrategy() = default;
};
// Конкретные стратегии
class CreditCardPayment : public PaymentStrategy {
public:
void pay(int amount) override {
std::cout << "Paid " << amount << " via Credit Card\n";
}
};
class PayPalPayment : public PaymentStrategy {
public:
void pay(int amount) override {
std::cout << "Paid " << amount << " via PayPal\n";
}
};
// Контекст
class ShoppingCart {
std::unique_ptr<PaymentStrategy> strategy;
public:
void setPaymentMethod(std::unique_ptr<PaymentStrategy> s) {
strategy = std::move(s);
}
void checkout(int total) {
if (strategy) strategy->pay(total);
}
};
int main() {
ShoppingCart cart;
cart.setPaymentMethod(std::make_unique<CreditCardPayment>());
cart.checkout(100);
cart.setPaymentMethod(std::make_unique<PayPalPayment>());
cart.checkout(50);
}Класс ShoppingCart не знает, какой метод оплаты он использует — он просто вызывает pay() у любой установленной стратегии. Вы можете менять стратегии во время выполнения с помощью setPaymentMethod(), что делает систему гибкой и легкой для расширения новыми способами оплаты.
Используйте паттерн Стратегия, когда у вас есть несколько алгоритмов для решения конкретной задачи и вы хотите динамически переключаться между ними, или когда вы хотите избежать использования условных операторов для выбора нужного поведения.
Задание
ЛегкоДавайте создадим Калькулятор доставки, который использует паттерн Стратегия для расчета стоимости доставки на основе различных методов отправки. Это практический сценарий, в котором вам нужно менять алгоритмы во время выполнения программы — одна и та же посылка может быть отправлена наземным, воздушным или экспресс-методом, каждый из которых имеет свою логику ценообразования.
Вы организуете свой код в трех файлах:
ShippingStrategy.h: Определите интерфейс стратегии и конкретные стратегии доставки.Создайте абстрактный класс
ShippingStrategyс чисто виртуальным методомcalculateCost(double weight), который возвращает стоимость доставки в виде double, а также виртуальный деструктор.Затем реализуйте три конкретные стратегии:
GroundShipping— стоимость1.5за единицу веса (weight * 1.5)AirShipping— стоимость4.0за единицу веса (weight * 4.0)ExpressShipping— стоимость6.5за единицу веса плюс фиксированная плата10.0(weight * 6.5 + 10.0)
ShippingService.h: Создайте класс контекста, который использует стратегию доставки.Ваш класс
ShippingServiceдолжен содержатьstd::unique_ptr<ShippingStrategy>в качестве приватного члена. Реализуйте:- Метод
setStrategy(std::unique_ptr<ShippingStrategy> strategy)для изменения метода доставки - Метод
calculateShipping(double weight), который использует текущую стратегию для вычисления и возврата стоимости
Если при вызове
calculateShippingстратегия не установлена, верните0.0.- Метод
main.cpp: Продемонстрируйте переключение стратегий во время выполнения.Считайте два входных значения:
- Вес посылки (double)
- Метод доставки:
ground,airилиexpress
Создайте
ShippingServiceи установите соответствующую стратегию на основе введенного метода. Рассчитайте и выведите стоимость доставки.Затем переключитесь на другую стратегию (используйте
air, если на входе было неair, в противном случае используйтеground) и снова рассчитайте стоимость для того же веса. Это демонстрирует возможность смены стратегий во время выполнения.Выведите каждую стоимость на отдельной строке ровно с одним десятичным знаком, с префиксом в виде названия метода:
[Method]: $[cost]
Например, при входных данных 5.0 и ground:
Ground: $7.5
Air: $20.0При входных данных 3.0 и express:
Express: $29.5
Air: $12.0При входных данных 10.0 и air:
Air: $40.0
Ground: $15.0Обратите внимание, что ShippingService не нужно знать детали каждого алгоритма ценообразования — он просто делегирует задачу той стратегии, которая установлена в данный момент. Вы можете легко добавлять новые методы доставки (например, доставку дроном или доставку в тот же день), вообще не изменяя класс сервиса.
Шпаргалка
Паттерн Стратегия (Strategy) определяет семейство алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми. Это позволяет изменять поведение объекта во время выполнения программы без изменения его исходного кода.
Паттерн состоит из трех частей:
- Интерфейс стратегии (Strategy interface): абстрактный класс, объявляющий метод алгоритма.
- Конкретные стратегии (Concrete Strategies): классы, реализующие различные вариации алгоритма.
- Контекст (Context): класс, который использует стратегию и может переключаться между ними.
// Strategy interface
class PaymentStrategy {
public:
virtual void pay(int amount) = 0;
virtual ~PaymentStrategy() = default;
};
// Concrete strategies
class CreditCardPayment : public PaymentStrategy {
public:
void pay(int amount) override {
std::cout << "Paid " << amount << " via Credit Card\n";
}
};
class PayPalPayment : public PaymentStrategy {
public:
void pay(int amount) override {
std::cout << "Paid " << amount << " via PayPal\n";
}
};
// Context
class ShoppingCart {
std::unique_ptr<PaymentStrategy> strategy;
public:
void setPaymentMethod(std::unique_ptr<PaymentStrategy> s) {
strategy = std::move(s);
}
void checkout(int total) {
if (strategy) strategy->pay(total);
}
};Контекст не знает, какую именно конкретную стратегию он использует — он просто вызывает метод интерфейса. Стратегии можно менять во время выполнения с помощью метода-сеттера:
ShoppingCart cart;
cart.setPaymentMethod(std::make_unique<CreditCardPayment>());
cart.checkout(100);
cart.setPaymentMethod(std::make_unique<PayPalPayment>());
cart.checkout(50);Когда использовать: Используйте Стратегию, когда у вас есть несколько алгоритмов для конкретной задачи и вы хотите динамически переключаться между ними, или когда вы хотите избежать использования условных операторов для выбора поведения.
Попробуйте сами
#include <iostream>
#include <string>
#include <iomanip>
#include <memory>
#include "ShippingStrategy.h"
#include "ShippingService.h"
int main() {
double weight;
std::string method;
std::cin >> weight;
std::cin >> method;
// Установить вывод с 1 десятичным знаком
std::cout << std::fixed << std::setprecision(1);
ShippingService service;
// TODO: На основе метода ввода ("ground", "air" или "express"):
// 1. Установить соответствующую стратегию в service
// 2. Рассчитать и вывести стоимость в формате: "[Method]: $[cost]"
// TODO: Переключиться на другую стратегию:
// - Если вводом был "air", переключиться на GroundShipping
// - В противном случае переключиться на AirShipping
// Рассчитать и вывести новую стоимость
return 0;
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП
Внешние файлыСборка и компиляция C++Заголовочные файлы и файлы исходного кодаПространства имен и область видимостиВведение в ООП на C++Классы и объектыУказатель 'this'Методы (функции-члены)Атрибуты (члены данных)Основы конструкторов и деструкторовИтоги — Простой калькулятор4Свойства классов
Члены экземпляра против статическихГеттеры и сеттерыКонстантные функции-членыКлючевое слово mutableСтатические методы и переменныеДружественные функции и классыИтоги — Менеджер банковских счетов7Наследование
Основы наследованияУровни доступа при наследованииПорядок вызова конструкторов и деструкторовПереопределение методовВиртуальные функции и VTableМножественное наследованиеВиртуальное наследованиеИтоги — Иерархия сотрудников10Обзор STL
Обзор и философия STLКонтейнеры STLИтераторыАлгоритмы STLФункторы и лямбда-выраженияИтоги: частота слов13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonFactory и Abstract FactoryПаттерн BuilderПаттерн ObserverПаттерн Strategy2Управление памятью
Стек и кучаУказатели и ссылкиДинамическая память (new/delete)Умные указатели в C++RAII в C++Итоги — Менеджер динамического массива5Инкапсуляция
Спецификаторы доступа в C++Спецификаторы доступа: подробный разборСокрытие данныхStruct против ClassВложенные и внутренние классыИтоги — Система учета студентов8Полиморфизм
Полиморфизм компиляции и времени выполненияПерегрузка функцийВиртуальные функции: повторениеЧистые виртуальные функцииАбстрактные классыПроектирование интерфейсов на C++Dynamic Casting и RTTIИтоги: Калькулятор фигур11Продвинутые концепции ООП
Композиция против наследованияМиксины через CRTPИдиома PimplСтирание типовEnum Classes и строгая типизацияОбработка исключений в ООППользовательские иерархии исключений14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикRAII как паттерн3Конструкторы и деструкторы
Конструктор по умолчаниюПараметризованный конструкторКонструктор копированияКонструктор перемещенияСписки инициализации конструктораДелегирующие конструкторыГлубокое погружение в деструкторыПравило трех / пяти / нуляПовторение — класс String6Перегрузка операторов
Введение в перегрузку операторовПерегрузка арифметических операторовПерегрузка операторов сравненияОператоры потоковПерегрузка оператора присваиванияПерегрузка операторов [] и ()Операторы преобразования типовИтоги — класс Matrix9Шаблоны
Шаблоны функцийШаблоны классовСпециализация шаблоновВариативные шаблоныОсновы SFINAE и Type TraitsИтоги — Обобщенный контейнер