Wzorzec Strategy
Część sekcji Programowanie obiektowe ścieżki C++ w Coddy. Lekcja 95 z 104.
Wzorzec strategii definiuje rodzinę algorytmów, hermetyzuje każdy z nich i umożliwia ich wzajemną wymianę. Pozwala to zmieniać zachowanie obiektu w czasie działania bez modyfikowania jego kodu — algorytm zmienia się niezależnie od klientów, którzy z niego korzystają.
Wzorzec składa się z trzech części: interfejsu strategii deklarującego metodę algorytmu, konkretnych strategii implementujących różne warianty oraz kontekstu, który korzysta ze strategii:
#include <iostream>
#include <memory>
// Interfejs strategii
class PaymentStrategy {
public:
virtual void pay(int amount) = 0;
virtual ~PaymentStrategy() = default;
};
// Konkretne strategie
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";
}
};
// Kontekst
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);
}Obiekt ShoppingCart nie wie, z jakiej metody płatności korzysta — po prostu wywołuje pay() na aktualnie ustawionej strategii. Możesz zmieniać strategie w czasie działania programu za pomocą setPaymentMethod(), dzięki czemu system jest elastyczny i łatwo go rozszerzać o nowe opcje płatności.
Użyj wzorca Strategia, gdy masz wiele algorytmów do wykonania określonego zadania i chcesz dynamicznie przełączać się między nimi lub gdy chcesz uniknąć instrukcji warunkowych służących do wyboru zachowania.
Wyzwanie
ŁatwyStwórzmy Kalkulator kosztów wysyłki, który wykorzystuje wzorzec Strategia do obliczania kosztów dostawy na podstawie różnych metod wysyłki. To praktyczny scenariusz, w którym trzeba zmieniać algorytmy w czasie działania programu: tę samą paczkę można wysłać transportem lądowym, lotniczym lub ekspresowym, a każda z tych metod ma własną logikę naliczania opłat.
Rozmieścisz kod w trzech plikach:
ShippingStrategy.h: Zdefiniuj interfejs strategii i konkretne strategie wysyłki.Utwórz abstrakcyjną klasę
ShippingStrategyz czysto wirtualną metodącalculateCost(double weight), która zwraca koszt wysyłki jako wartość typu double, oraz wirtualnym destruktorem.Następnie zaimplementuj trzy konkretne strategie:
GroundShipping: koszt wynosi1.5za jednostkę masy (weight * 1.5)AirShipping: koszt wynosi4.0za jednostkę masy (weight * 4.0)ExpressShipping: koszt wynosi6.5za jednostkę masy plus stała opłata10.0(weight * 6.5 + 10.0)
ShippingService.h: Utwórz klasę kontekstu, która korzysta ze strategii wysyłki.Klasa
ShippingServicepowinna przechowywaćstd::unique_ptr<ShippingStrategy>jako prywatne pole. Zaimplementuj:- Metodę
setStrategy(std::unique_ptr<ShippingStrategy> strategy), która zmienia metodę wysyłki - Metodę
calculateShipping(double weight), która korzysta z bieżącej strategii, aby obliczyć i zwrócić koszt
Jeśli podczas wywołania
calculateShippingnie ustawiono żadnej strategii, zwróć0.0.- Metodę
main.cpp: Zademonstruj zmianę strategii w czasie działania programu.Wczytaj dwa dane wejściowe:
- Masę paczki (double)
- Metodę wysyłki:
ground,airlubexpress
Utwórz obiekt
ShippingServicei ustaw odpowiednią strategię na podstawie wybranej metody. Oblicz i wypisz koszt wysyłki.Następnie przełącz się na inną strategię (użyj
air, jeśli podana metoda nie byłaair; w przeciwnym razie użyjground) i ponownie oblicz koszt dla tej samej masy. Zademonstrujesz w ten sposób zaletę zmieniania strategii w czasie działania programu.Wypisz każdy koszt w osobnym wierszu, z dokładnością do jednego miejsca po przecinku i z nazwą metody na początku:
[Method]: $[cost]
Na przykład dla danych wejściowych 5.0 i ground:
Ground: $7.5
Air: $20.0Dla danych wejściowych 3.0 i express:
Express: $29.5
Air: $12.0Dla danych wejściowych 10.0 i air:
Air: $40.0
Ground: $15.0Zauważ, że ShippingService nie musi znać szczegółów poszczególnych algorytmów naliczania opłat. Po prostu przekazuje obliczenia do aktualnie ustawionej strategii. Możesz łatwo dodać nowe metody wysyłki (takie jak dostawa dronem lub dostawa tego samego dnia) bez wprowadzania jakichkolwiek zmian w klasie usługi.
Spróbuj swoich sił
#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;
// Ustaw wyświetlanie wyniku z 1 miejscem po przecinku
std::cout << std::fixed << std::setprecision(1);
ShippingService service;
// TODO: Na podstawie wybranej metody ("ground", "air" lub "express"):
// 1. Ustaw odpowiednią strategię w obiekcie service
// 2. Oblicz i wyświetl koszt w formacie: "[Method]: $[cost]"
// TODO: Zmień strategię na inną:
// - Jeśli wprowadzono "air", przełącz na GroundShipping
// - W przeciwnym razie przełącz na AirShipping
// Oblicz i wyświetl nowy koszt
return 0;
}
Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.
Wszystkie lekcje w sekcji Programowanie obiektowe
1Podstawy programowania obiektowego
Pliki zewnętrzneBudowanie i kompilacja C++Pliki nagłówkowe i pliki źródłowePrzestrzenie nazw i zakresWprowadzenie do programowania obiektowego w C++Klasy a obiektyWskaźnik „this”Metody (funkcje składowe)Atrybuty (składowe danych)Podstawy konstruktorów i destruktorówPowtórzenie – prosty kalkulator4Właściwości klas
Elementy instancji a elementy statyczneGettery i setteryStałe funkcje składoweSłowo kluczowe mutableMetody i zmienne statyczneFunkcje i klasy zaprzyjaźnionePodsumowanie – menedżer konta bankowego7Dziedziczenie
Podstawy dziedziczeniaPoziomy dostępu w dziedziczeniuKolejność wywołań konstruktora i destruktoraNadpisywanie metodFunkcje wirtualne i VTableWielokrotne dziedziczenieDziedziczenie wirtualnePowtórzenie — hierarchia pracowników10Przegląd STL
Przegląd i filozofia STLKontenery STLIteratoryAlgorytmy STLFunktory i wyrażenia lambdaPowtórzenie – częstość występowania słów13Wzorce projektowe, część 1
Wprowadzenie do wzorców projektowychWzorzec SingletonFabryka i fabryka abstrakcyjnaWzorzec BuilderWzorzec ObserverWzorzec Strategy2Zarządzanie pamięcią
Pamięć stosu a stertyWskaźniki i referencjePamięć dynamiczna (new/delete)Inteligentne wskaźniki w C++RAII w C++Podsumowanie — menedżer tablicy dynamicznej5Hermetyzacja
Specyfikatory dostępu w C++Specyfikatory dostępu — szczegółowoUkrywanie informacjiStruktura a klasaKlasy zagnieżdżone i wewnętrznePodsumowanie — system ewidencji studentów8Polimorfizm
Polimorfizm czasu kompilacji i wykonaniaPrzeciążanie funkcjiFunkcje wirtualne — powtórkaCzysto wirtualne funkcjeKlasy abstrakcyjneProjektowanie interfejsów w C++Rzutowanie dynamiczne i RTTIPodsumowanie — kalkulator kształtów3Konstruktory i destruktory
Konstruktor domyślnyKonstruktor z parametramiKonstruktor kopiującyKonstruktor przenoszącyListy inicjalizacyjne konstruktoraKonstruktory delegująceDestruktor — szczegółowe omówienieZasada trzech / pięciu / zeraPodsumowanie — klasa String6Przeciążanie operatorów
Wprowadzenie do przeciążania operatorówPrzeciążanie operatorów arytmetycznychPrzeciążanie operatorów porównaniaOperatory strumieniowePrzeciążanie operatora przypisaniaPrzeciążanie operatorów [] i ()Operatory konwersji typówPodsumowanie — klasa Matrix9Szablony
Szablony funkcjiSzablony klasSpecjalizacja szablonówSzablony wariadycznePodstawy SFINAE i cech typówPodsumowanie — kontener generycznyPoćwicz samodzielnie: Kompilator C++ online