Menu
Coddy logo textTech

Паттерн 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(), что делает систему гибкой и легкой для расширения новыми способами оплаты.

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

challenge icon

Задание

Легко

Давайте создадим Калькулятор доставки, который использует паттерн Стратегия для расчета стоимости доставки на основе различных методов отправки. Это практический сценарий, в котором вам нужно менять алгоритмы во время выполнения программы — одна и та же посылка может быть отправлена наземным, воздушным или экспресс-методом, каждый из которых имеет свою логику ценообразования.

Вы организуете свой код в трех файлах:

  • 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: Продемонстрируйте переключение стратегий во время выполнения.

    Считайте два входных значения:

    1. Вес посылки (double)
    2. Метод доставки: 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;
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование