Wzorzec Strategy opisuje rodzinę algorytmów, zamyka każdy z nich w osobnym obiekcie i pozwala je wymieniać. Dzięki temu algorytm zmienia się niezależnie od kodu, który go używa. Wybór zachowania możesz podjąć na etapie kompilacji (szablony) albo w czasie wykonania (funkcje wirtualne lub wskaźniki funkcji).
Problem
Masz klasę, która wykonuje tę samą operację na kilka sposobów. Najprostsze podejście wpisuje wszystkie warianty na sztywno w logikę warunkową:
class DataProcessor {
public:
void process(Data& data, const std::string& algorithm) {
if (algorithm == "quick_sort") {
// implementacja quick sort
} else if (algorithm == "merge_sort") {
// implementacja merge sort
} else if (algorithm == "heap_sort") {
// implementacja heap sort
}
}
};
To podejście ma kilka problemów:
- Procesor jest ściśle powiązany ze wszystkimi algorytmami.
- Dodanie nowego algorytmu wymaga zmiany klasy procesora.
- Klasa łamie zasadę pojedynczej odpowiedzialności, bo zajmuje się kilkoma zagadnieniami naraz.
- Łamie też zasadę otwarte-zamknięte, bo każda zmiana wymaga modyfikacji istniejącego kodu.
- Z każdym nowym algorytmem klasa rośnie i trudniej ją utrzymać.
Wzorzec Strategy rozwiązuje ten problem, przenosząc algorytmy do osobnych, wymiennych obiektów.
Analogia z życia
Pomyśl o transporcie na lotnisko:
- Cel: dostać się na lotnisko.
- Strategie: samochód, taksówka, transport publiczny albo rower.
- Elastyczność: wybór zależy od sytuacji, czyli czasu, kosztu i pogody.
- Wymienność: każda strategia dowiezie Cię na miejsce, więc możesz ją zmienić bez zmiany całego planu podróży.
- Enkapsulacja: szczegóły zostają w środku strategii, na przykład trasa, korki i zakup biletu.
Kluczowe koncepcje
Dwa podejścia do implementacji
Wzorzec Strategy ma dwa główne style implementacji:
- Oparte na szablonach (Kompilacja): algorytmy są wybierane przez szablony C++, a typ jest znany w czasie kompilacji.
- Wybór w Czasie Wykonania: algorytmy są wybierane przez funkcje wirtualne lub wskaźniki funkcji, a typ jest ustalany podczas wykonania.
Poziomy enkapsulacji
- Każdy algorytm to osobna klasa albo szablon.
- Klient używający strategii nie musi wiedzieć o pozostałych strategiach.
- Różne strategie mogą mieć zupełnie inne interfejsy wewnętrzne.
Kompromisy
| Aspekt | Korzyść | Koszt |
|---|---|---|
| Izolacja algorytmu | Każdy algorytm jest niezależny i testowalny | Więcej klas i struktur |
| Elastyczność w czasie wykonania | Przełączaj algorytmy w czasie wykonania (podejście dynamiczne) | Narzut funkcji wirtualnej, pośrednie wywołanie przez wskaźnik |
| Bezpieczeństwo kompilacji | Bezpieczeństwo typów, zero narzutu w czasie wykonania (podejście szablonowe) | Algorytm musi być znany w czasie kompilacji, duplikacja kodu szablonu |
| Rozszerzalność | Dodawanie algorytmów nie modyfikuje istniejącego kodu | Wymaga tworzenia nowych klas strategii |
| Testowalność | Algorytmy można testować w izolacji | Więcej obiektów do skonfigurowania |
Kiedy używać Strategy
Dobre przypadki użycia:
- Kilka algorytmów dla tego samego zadania: sortowanie, kompresja, serializacja.
- Wybór zależy od danych albo konfiguracji w czasie wykonania.
- Chcesz przełączać algorytmy ze względu na wydajność lub zachowanie.
- Chcesz uniknąć logiki warunkowej u klienta.
- Algorytmy trzeba testować osobno.
Rozważ podejście szablonowe, gdy:
- Algorytm jest znany w czasie kompilacji.
- Nie chcesz płacić narzutu funkcji wirtualnej.
- Wydajność ma znaczenie.
- Wszystkie ścieżki kodu są znane w czasie kompilacji.
Rozważ wybór w czasie wykonania, gdy:
- Algorytm wybierasz w trakcie działania programu.
- Strategię określają pliki konfiguracyjne lub zmienne środowiskowe.
- O wyborze decydują dane od użytkownika.
- Architektura opiera się na wtyczkach i strategie ładują się dynamicznie.
Unikaj, gdy:
- Masz tylko jeden algorytm.
- Algorytmy rzadko się zmieniają.
- Kontekst jest prosty i logika warunkowa czyta się lepiej niż abstrakcja.
Powiązane wzorce
- State: podobna struktura, ale zmienia zachowanie obiektu wraz z jego stanem wewnętrznym.
- Decorator: też opakowuje zachowanie, ale dodaje funkcje; Strategy wymienia cały algorytm.
- Factory: często łączy się ze Strategy, żeby utworzyć właściwą strategię.
- Template Method: odwrotne podejście; klasa bazowa definiuje szkielet algorytmu, a podklasy wypełniają kroki.
Style implementacji
Sposób implementacji zależy od tego, czego potrzebujesz:
- Policy Based Design: zaawansowana technika C++ oparta na szablonach; strategie są parametrami szablonu (cechami lub politykami).
- Wybór Oparty na Szablonach: wybór strategii w czasie kompilacji przez specjalizację szablonu.
- Wybór w Czasie Wykonania: funkcje wirtualne lub wskaźniki funkcji wybierają strategię w czasie wykonania.
Podsumowanie
Po Strategy sięgasz, gdy klasa zaczyna puchnąć od if-ów wybierających algorytm. Szablony wygrywają, gdy algorytm jest znany przy kompilacji. Funkcje wirtualne wygrywają, gdy o wyborze decyduje konfiguracja albo użytkownik. Policy Based Design idzie dalej, ale ma sens głównie w bibliotekach, które stać na dłuższą kompilację.