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:

  1. Oparte na szablonach (Kompilacja): algorytmy są wybierane przez szablony C++, a typ jest znany w czasie kompilacji.
  2. 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:

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ę.