1. Definicja
Strategy (Strategia) to wzorzec behawioralny, który definiuje rodzinę algorytmów, hermetyzuje każdy z nich osobno i sprawia, że są one wzajemnie wymienne w trakcie działania programu.
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it. — Gang of Four, Design Patterns
Najprościej rzecz ujmując: ten sam cel, różne sposoby jego osiągnięcia — wybierane w locie.
Zamiast zaszywać w jednej klasie serię instrukcji if-else czy switch, które decydują "jak coś zrobić", wydzielamy
każdy sposób działania do osobnej klasy implementującej wspólny interfejs. Klasa korzystająca ze strategii nie wie i nie musi wiedzieć, jak dokładnie algorytm
został zrealizowany, wie tylko, że może go wywołać.
2. Przykład z życia: zobaczmy jak to zrozumieć?
Wyobraźmy sobie, że planujemy podróż i otwieramy aplikację nawigacyjną. Wpisujemy cel podróży, a następnie wybieramy sposób dotarcia do niego: samochodem, pieszo, rowerem albo komunikacją miejską.
Punkt startowy i punkt docelowy są takie same. To, co się zmienia, to strategia wyznaczania trasy:
- nawigacja samochodowa unika ulic jednokierunkowych pod prąd i uwzględnia korki,
- trasa piesza może prowadzić przez parki i skróty niedostępne dla aut,
- trasa rowerowa priorytetyzuje ścieżki rowerowe,
- trasa komunikacją miejską uwzględnia rozkłady jazdy autobusów i tramwajów.
Aplikacja nawigacyjna (kontekst) nie musi wiedzieć, w jaki sposób każdy z tych algorytmów wylicza trasę. Wystarczy, że zna jeden wspólny "kontrakt": podaj punkt A i B, zwróć trasę. Wybór konkretnej strategii następuje dopiero w momencie, gdy użytkownik kliknie odpowiednią ikonkę — samochodu, pieszego czy roweru.
Ćwiczenie myślowe: wyobraźmy sobie, że ktoś zaimplementował tę nawigację jako jedną ogromną metodę wyznaczTrase(), wewnątrz której znajduje się blok if (typTransportu == SAMOCHOD) {...} else if (typTransportu == ROWER) {...} i tak dalej. Co się stanie, gdy producent aplikacji zechce dodać nawigację dla hulajnogi elektrycznej? Trzeba będzie otworzyć tę samą metodę, zrozumieć cały istniejący kod i dopisać kolejny warunek, ryzykując zepsucie istniejącej logiki. Właśnie ten problem rozwiązuje Strategia.
Dokładnie ten sam mechanizm przenosimy teraz do naszego sklepu internetowego, który rozwijaliśmy przy okazji SRP.
3. Przykład - naruszenie zasady Strategii
Rozwijamy nasz sklep internetowy i musimy obsłużyć wiele metod płatności: kartą, przelewem, BLIK-iem oraz PayPal.
Naturalną, ale niebezpieczną pokusą jest umieszczenie całej logiki w jednej klasie:
class PaymentProcessor {
public void processPayment(String paymentType, double amount) {
// Klasa musi znać WSZYSTKIE możliwe sposoby płatności
if (paymentType.equals("KARTA")) {
System.out.println("Autoryzacja karty na kwotę: " + amount);
System.out.println("Łączenie z bramką płatności bankowej...");
// logika autoryzacji karty
} else if (paymentType.equals("BLIK")) {
System.out.println("Generowanie kodu BLIK dla kwoty: " + amount);
// logika BLIK
} else if (paymentType.equals("PRZELEW")) {
System.out.println("Generowanie numeru konta do przelewu na: " + amount);
// logika przelewu
} else if (paymentType.equals("PAYPAL")) {
System.out.println("Przekierowanie do PayPal, kwota: " + amount);
// logika PayPal
} else {
throw new IllegalArgumentException("Nieznana metoda płatności");
}
}
}
Problemy, jakie to powoduje, są wielorakie:
- Naruszenie Open/Closed — dodanie nowej metody płatności (np. Apple Pay) wymaga edycji istniejącej klasy
PaymentProcessor, zamiast jedynie dopisania nowego kodu. - Rosnąca złożoność cyklomatyczna — każdy kolejny
else ifzwiększa liczbę możliwych ścieżek wykonania, co utrudnia testowanie i zrozumienie kodu. - Brak izolacji — błąd we implementacji BLIK-a (np. literówka w warunku) znajduje się w tym samym pliku co logika PayPal, więc łatwo przypadkiem coś zepsuć podczas edycji.
- Trudne testowanie jednostkowe — aby przetestować logikę PayPal, musimy uruchomić całą metodę
processPayment()z odpowiednim parametremString, zamiast przetestować wyizolowaną klasę. - Duplikacja przy podobnym kodzie w innych miejscach — jeśli gdzie indziej w systemie (np. przy zwrotach) potrzebujemy podobnej logiki wyboru metody płatności, kopiujemy ten sam łańcuch
if-elseponownie.
4. Przykład - zgodny ze Strategią
Wydzielmy każdą metodę płatności do osobnej klasy implementującej wspólny interfejs PaymentStrategy.
// Wspólny "kontrakt" dla wszystkich metod płatności.
// Kontekst (PaymentProcessor) będzie znał tylko tę metodę.
interface PaymentStrategy {
void pay(double amount);
}
class CardPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Autoryzacja karty na kwotę: " + amount);
// logika autoryzacji karty - izolowana i testowalna osobno
}
}
class BlikPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Generowanie kodu BLIK dla kwoty: " + amount);
}
}
class BankTransferPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Generowanie numeru konta do przelewu na: " + amount);
}
}
class PayPalPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Przekierowanie do PayPal, kwota: " + amount);
}
}
// Kontekst - nie zna szczegółów działania żadnej strategii,
// jedynie deleguje wywołanie do aktualnie ustawionej implementacji.
class PaymentProcessor {
private PaymentStrategy strategy;
// Strategia jest wstrzykiwana z zewnątrz (np. na podstawie wyboru klienta w koszyku)
public PaymentProcessor(PaymentStrategy strategy) {
this.strategy = strategy;
}
// Umożliwia zmianę strategii w trakcie działania programu
public void setStrategy(PaymentStrategy strategy) {
this.strategy = strategy;
}
public void processPayment(double amount) {
strategy.pay(amount);
}
}
// Użycie - klient wybiera BLIK w koszyku
PaymentProcessor processor = new PaymentProcessor(new BlikPayment());
processor.processPayment(129.99);
// Klient zmienia zdanie i wybiera kartę - podmieniamy strategię w locie
processor.setStrategy(new CardPayment());
processor.processPayment(129.99);
Co zmieniliśmy krok po kroku?
- Wydzieliliśmy wspólny interfejs
PaymentStrategy— to "kontrakt", którego przestrzega każda metoda płatności. - Każda konkretna metoda płatności (
CardPayment,BlikPayment,BankTransferPayment,PayPalPayment) stała się osobną, niezależną klasą. - Klasa
PaymentProcessor(kontekst) nie zawiera już żadnej logikiif-else— jedynie przechowuje referencję do aktualnej strategii i deleguje do niej wywołanie. - Dodanie Apple Pay oznacza teraz dopisanie nowej klasy
ApplePayPayment implements PaymentStrategy, bez dotykania istniejącego kodu — dokładnie zgodnie z zasadą Open/Closed.
5. Diagram struktury
┌───────────────────┐ ┌───────────────────────┐ │ PaymentProcessor │──────▶│ <<interface>> │ │ (Context) │ używa │ PaymentStrategy │ │ │ │ + pay(amount): void │ └───────────────────┘ └────────────────────────┘ ▲ ┌──────────────────┼────────────────────┬─────────────────┐ │ │ │ │ ┌──────────────────┐ ┌───────────────┐ ┌────────────────────┐ ┌──────────────┐ │ CardPayment │ │ BlikPayment │ │ BankTransferPayment│ │ PayPalPayment│ └──────────────────┘ └───────────────┘ └────────────────────┘ └──────────────┘
Kontekst trzyma referencję do interfejsu, nigdy do konkretnej implementacji. Dzięki temu strategie można podmieniać jak baterie w latarce — bez ingerencji w samą latarkę.
6. Dlaczego Strategia jest ważna? Kiedy NIE jej stosować?
Korzyści:
- Zgodność z Open/Closed — nowe algorytmy dodajemy przez nowe klasy, nie modyfikując istniejącego kodu.
- Eliminacja rozbudowanych warunków — koniec z rozrastającymi się blokami
if-elseczyswitch, które trudno czytać i testować. - Niezależny rozwój i testowanie — każdą strategię można rozwijać, testować i wdrażać osobno, co ułatwia pracę w zespole (mniej konfliktów przy mergowaniu, jasny podział odpowiedzialności między programistami).
- Elastyczność w runtime — strategię można podmienić w trakcie działania programu, np. na podstawie preferencji użytkownika czy konfiguracji.
Kiedy NIE stosować Strategii:
- Gdy mamy tylko jeden, stały sposób wykonania algorytmu i nie przewidujemy żadnych wariantów — wtedy wzorzec wprowadza niepotrzebną złożoność (dodatkowy interfejs i klasy zamiast prostej metody).
- Gdy liczba strategii jest bardzo mała (np. dwie) i nie zmienia się w czasie — czasem prosty warunek
if-elsejest czytelniejszy niż rozbudowana hierarchia klas. - Gdy klient musi znać szczegóły każdej strategii, by dokonać wyboru — wzorzec zakłada, że wybór strategii jest podejmowany raz, na zewnątrz, a nie ciągle negocjowany przez klienta kontekstu.
Rzeczywiste konsekwencje zaniedbania: w dużych systemach e-commerce klasa realizująca płatności "na piechotę" (jak w naruszeniu powyżej) potrafi urosnąć do tysięcy linii kodu, gdy dochodzą kolejne rynki, waluty i dostawcy płatności. Każda zmiana staje się ryzykowna, a testowanie pojedynczej metody płatności wymaga uruchomienia całej klasy.
7. Kiedy stosować? Przykłady z frameworków
Strategia sprawdza się szczególnie dobrze, gdy:
- System płatności/dostaw — różne metody płatności (jak w naszym przykładzie) lub różne firmy kurierskie z odmienną logiką kalkulacji kosztu wysyłki.
- Systemy sortowania i porównywania danych — różne kryteria sortowania listy (po cenie, po dacie, po popularności).
- Walidacja danych — różne reguły walidacji formularza w zależności od typu konta użytkownika.
- Systemy rabatowe — różne strategie naliczania zniżek (procentowa, kwotowa, "kup 2 zapłać za 1").
W praktyce JDK i frameworków:
java.util.Comparatorto klasyczny przykład Strategii wbudowanej w JDK — metodaCollections.sort(list, comparator)przyjmuje strategię porównywania jako parametr, nie wiedząc nic o konkretnej logice sortowania.java.io.FileFilteriFilenameFilterpozwalają wstrzyknąć różne strategie filtrowania plików.- W Springu wzorzec ten leży u podstaw takich mechanizmów jak
PasswordEncoder— możemy podmienić strategię kodowania haseł (BCrypt, Argon2, SCrypt) bez zmiany kodu, który z niej korzysta.
Powiązane wzorce
Strategia bywa mylona z Template Method, ponieważ oba wzorce dotyczą "rodzin algorytmów". Kluczowa różnica:
- Strategy opiera się na kompozycji — cały algorytm jest wymieniany jako jeden obiekt, wstrzyknięty do kontekstu, a podmiana odbywa się w czasie działania programu.
- Template Method opiera się na dziedziczeniu — szkielet algorytmu jest stały i zdefiniowany w klasie bazowej, a podklasy nadpisują jedynie wybrane kroki tego szkieletu.
Innymi słowy: Strategia pyta "jaki cały algorytm chcesz użyć?", a Template Method pyta "jak chcesz zrealizować ten jeden konkretny krok w z góry ustalonym procesie?".