Wzorzec Projektowy Strategy: Wymienne Algorytmy

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:

  1. Interfejs strategii (Strategy): Definiuje metodę wspólną dla wszystkich algorytmów.
  2. Konkretne strategie (ConcreteStrategy): Implementują interfejs, dostarczając różne algorytmy.
  3. Kontekst (Context): Klasa przechowująca strategię i delegująca do niej zadania.
  4. 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.

Java
// 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. Metoda setStrategy pozwala 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 CreditCardStrategy i przetwarza płatność.
  • Następnie przełącza na PayPalStrategy i 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.