Wzorzec Strategia (ang. Strategy Pattern) jest wzorcem behawioralnym, który umożliwia definiowanie rodziny algorytmów, enkapsulację każdego z nich w osobnej klasie oraz ich wymienność w czasie rzeczywistym. Wzorzec ten pozwala na oddzielenie logiki algorytmu od klasy kontekstu, która z niego korzysta, co zwiększa elastyczność systemu i ułatwia jego rozszerzanie. Jest szczególnie przydatny w sytuacjach, gdy różne warianty tego samego zadania muszą być wykonywane w zależności od kontekstu, a przełączanie między nimi powinno być płynne.
Kiedy Warto Użyć Wzorca Strategy?
Strategy jest szczególnie przydatny, gdy:
- Istnieje wiele podobnych algorytmów lub zachowań, które można wymieniać dynamicznie.
- Chcemy uniknąć warunkowych instrukcji (if-else lub switch) w kodzie kontekstu.
- Potrzebujemy izolować algorytmy od kontekstu, umożliwiając ich niezależne testowanie i modyfikację.
- System wymaga zgodności z zasadą otwarte/zamknięte, pozwalając na dodawanie nowych strategii bez zmian w istniejącym kodzie.
Przykładowo, w systemach e-commerce Strategy może służyć do wyboru różnych metod płatności w zależności od preferencji użytkownika, bez modyfikowania głównej logiki przetwarzania zamówienia.
Schematy Implementacji Wzorca Strategy
Wzorzec Strategy składa się z następujących elementów:
- Interfejs strategii (Strategy): Definiuje metodę wspólną dla wszystkich algorytmów.
- Konkretne strategie (ConcreteStrategy): Implementują interfejs, dostarczając różne algorytmy.
- Kontekst (Context): Klasa przechowująca strategię i delegująca do niej zadania.
- Klient: Kod konfigurujący kontekst i wybierający strategię.
Implementacja jest zazwyczaj prosta, z naciskiem na polimorfizm i dekoplowanie.
Przykład Kodu: Przetwarzanie Płatności
Własny przykład w Java: zamiast gotowania jajek, użyjemy strategii płatności w e-commerce. Interfejs PaymentStrategy definiuje metodę pay(double amount). Konkretne strategie to CreditCardStrategy i PayPalStrategy. Kontekst to PaymentProcessor, który deleguje przetwarzanie płatności.
// Interfejs strategii
public interface PaymentStrategy {
void pay(double amount);
}
// Konkretne strategie
public class CreditCardStrategy implements PaymentStrategy {
private String cardNumber;
public CreditCardStrategy(String cardNumber) {
this.cardNumber = cardNumber;
}
@Override
public void pay(double amount) {
System.out.println("Płatność kartą kredytową: " + amount + " PLN, numer karty: " + cardNumber);
}
}
public class PayPalStrategy implements PaymentStrategy {
private String email;
public PayPalStrategy(String email) {
this.email = email;
}
@Override
public void pay(double amount) {
System.out.println("Płatność PayPal: " + amount + " PLN, email: " + email);
}
}
// Kontekst
public class PaymentProcessor {
private PaymentStrategy strategy;
public void setStrategy(PaymentStrategy strategy) {
this.strategy = strategy;
}
public void processPayment(double amount) {
if (strategy == null) {
throw new IllegalStateException("Strategia płatności nie została ustawiona!");
}
strategy.pay(amount);
}
}
// Klient
public class Main {
public static void main(String[] args) {
PaymentProcessor processor = new PaymentProcessor();
// Ustawienie strategii karty kredytowej
processor.setStrategy(new CreditCardStrategy("1234-5678-9012-3456"));
processor.processPayment(150.0); // Wyjście: Płatność kartą kredytową: 150.0 PLN, numer karty: 1234-5678-9012-3456
// Zmiana strategii na PayPal
processor.setStrategy(new PayPalStrategy("klient@example.com"));
processor.processPayment(200.0); // Wyjście: Płatność PayPal: 200.0 PLN, email: klient@example.com
}
}Analiza i Wyjaśnienie Działania
- Interfejs strategii (PaymentStrategy): Definiuje metodę
pay(double amount), wspólną dla wszystkich strategii płatności. Zapewnia spójny interfejs, umożliwiając polimorficzne użycie. - Konkretne strategie (CreditCardStrategy, PayPalStrategy): Implementują
pay, dostarczając specyficzne algorytmy przetwarzania płatności. Każda strategia jest samodzielna i może być rozszerzana niezależnie. - Kontekst (PaymentProcessor): Przechowuje referencję do strategii i deleguje do niej zadanie poprzez
processPayment. MetodasetStrategypozwala na dynamiczną zmianę. Dodano walidację, aby uniknąć NullPointerException. - Klient (Main): Konfiguruje kontekst, ustawia strategie i wywołuje przetwarzanie. Demonstruje wymienność w runtime.
Analiza: Wzorzec umożliwia łatwe dodanie nowej strategii (np. BankTransferStrategy) bez modyfikacji PaymentProcessor. Brak przekazywania dodatkowego kontekstu do strategii utrzymuje prostotę, ale w bardziej złożonych przypadkach można dodać parametry do metody pay.
Przykład Użycia
W kodzie powyżej, klient dynamicznie zmienia strategię:
- Najpierw ustawia
CreditCardStrategyi przetwarza płatność. - Następnie przełącza na
PayPalStrategyi przetwarza inną płatność.
To ilustruje płynną wymienność algorytmów bez zmian w kontekście.
Zalety i Wady Wzorca Strategy
Zalety
- Elastyczność i wymienność: Algorytmy można swappingować w runtime bez modyfikacji kontekstu.
- Dekoplowanie i izolacja: Logika algorytmów oddzielona od kontekstu, co ułatwia testowanie i utrzymanie.
- Unikanie warunkowych instrukcji: Zastępuje if-else lub switch enkapsulowanymi klasami.
- Zgodność z zasadami OOP: Wspiera otwarte/zamknięte i pojedynczej odpowiedzialności.
Wady
- Zwiększona złożoność: Więcej klas i interfejsów, co komplikuje prosty kod.
- Świadomość strategii przez klienta: Aplikacja musi znać wszystkie strategie do wyboru.
- Ograniczony kontekst: Strategie nie mają bezpośredniego dostępu do danych kontekstu, co może wymagać rozszerzeń.
Przykłady Zastosowania w Świecie Rzeczywistym
Wzorzec Strategy jest szeroko stosowany w praktyce. Na przykład:
- Systemy e-commerce – metody płatności: Wybór między kartą kredytową, PayPal czy przelewem bankowym w zależności od użytkownika.
- Nawigacja w aplikacjach: Różne algorytmy routingu (najkrótsza trasa, unikanie korków) w systemach jak Google Maps.
- Serwisy streamingowe: Różne poziomy subskrypcji z algorytmami cenowymi (basic, premium).
- Algorytmy sortowania: Wymiana sorterów (bubble sort, quick sort) w bibliotekach jak Java Collections.
- Rabaty w liniach lotniczych: Różne strategie cenowe w zależności od sezonu lub klienta.
Te przykłady pokazują, jak Strategy upraszcza zarządzanie wariantami zachowań w realnych aplikacjach.
Wnioski dla Wzorca Strategy
Wzorzec Strategy zapewnia elastyczność poprzez wymienialne algorytmy, dekoplowanie i unikanie warunkowych struktur. Idealny do scenariuszy z wariantami zachowań, jak płatności czy nawigacja. Mimo zalet jak zgodność z OOP, może zwiększać liczbę klas, dlatego stosuj go w złożonych systemach. Łączy się dobrze z Factory do tworzenia strategii dynamicznie.
Ogólne Wnioski
Wzorce Builder, Factory i Strategy uzupełniają się w projektowaniu oprogramowania: Builder skupia się na złożonej konstrukcji, Factory na elastycznym tworzeniu typów, a Strategy na wymiennych algorytmach. Wszystkie promują modularność, dekoplowanie i skalowalność, ale wymagają ostrożnego użycia, by nie komplikować kodu. W praktyce łącz je dla robustnych systemów, jak w e-commerce czy grach.