Wzorzec Strategia (Strategy Pattern) 

Wzorzec Strategia (Strategy Pattern) 

Wstęp

Witaj w świecie wzorców projektowych, gdzie programowanie staje się trochę jak gotowanie – czasem masz składniki, które się świetnie komponują, a czasem musisz walczyć z chaotycznym bałaganem w kuchni. Dzisiaj przyjrzymy się wzorcowi Strategia.

Wyobraź sobie, że jesteś szefem kuchni w zatłoczonej restauracji. Klienci chcą różnych dań, a Ty musisz wyczarować coś pysznego, nie zbaczając z kursu.
Co robisz? Oczywiście, korzystasz z różnych przepisów! Właśnie tak działa wzorzec Strategiapozwala Ci wybrać najlepszą metodę działania w danej chwili, bez potrzeby przerywania gotowania (czytania kodu).

Zamiast trzymać wszystkie swoje kulinarne tajemnice w jednym przepisie, wzorzec Strategia pozwala na ich rozdzielenie, tak aby każdy algorytm mógł działać w swojej własnej klasie. Dzięki temu możesz dodać nowe „przepisy” bez obaw o to, że zrujnuje to całą potrawę. A więc, gotowi na gotowanie z kodem? Zanurzmy się w świat wzorca Strategia!

Wzorzec Strategia

Wzorzec Strategia to sposób na to, aby móc łatwo zmieniać działanie programu bez potrzeby modyfikowania jego kodu. Dzięki temu możemy przygotować różne wersje rozwiązywania danego problemu, umieścić je w osobnych klasach, a później zamieniać między nimi w zależności od potrzeb, bez dotykania reszty aplikacji.

Analogia do prawdziwego życia

Wyobraź sobie, że musisz dotrzeć do supermarketu, bo kończy Ci się ulubiony sok pomarańczowy. Masz kilka opcji, jak tam dojechać. Możesz wskoczyć na rower, co jest super, o ile nie boisz się deszczu. Możesz złapać autobus, jeśli akurat nie odjedzie Ci sprzed nosa. Zawsze możesz zamówić taksówkę, choć portfel może wtedy poczuć się nieco lżej. A jeśli masz odrobinę szczęścia, może znajdziesz kolegę, który nie tylko Cię podwiezie, ale jeszcze pomoże z zakupami. To są Twoje strategie na dotarcie do sklepu. Wybór zależy od tego, jak bardzo się spieszysz, ile chcesz wydać albo jak bardzo chcesz oszczędzić na kaloriach!

Jak działa wzorzec Strategia?

Enkapsulacja algorytmów:

Zamiast wrzucać wszystko do jednego worka (czyli do jednej klasy), każdą metodę działania (algorytm) chowamy do osobnej klasy. Każda z tych klas działa według tego samego schematu – mają wspólny interfejs, jakby wszyscy grali według tych samych zasad gry, tylko każdy ma swój styl.

Elastyczność:

Wiesz, co jest fajne? Możesz zmieniać algorytmy w locie, bez potrzeby grzebania w kodzie! To jak zmiana przepisów na pizzę w kuchni – możesz zacząć od pepperoni, ale w połowie zamienić go na wegetariańską, nie przerywając pieczenia. Klient (czyli klasa, która korzysta z tych algorytmów) nie musi wiedzieć, jaki dokładnie algorytm jest używany – po prostu dostaje efekt końcowy. Życie staje się prostsze!

Interfejsy lub klasy abstrakcyjne:

Zamiast wymyślać za każdym razem coś nowego, wszystkie algorytmy bazują na tym samym „szablonie”. To tak, jakby wszystkie pizze miały wspólną bazę, a dodatki to już kwestia wyboru konkretnego przepisu. Dzięki temu, łatwo można dodać nowe przepisy (strategię), a cała reszta kodu nawet o tym nie wie!

Jak to wygląda w kodzie?

Klient, czyli klasa, która chce użyć jakiegoś algorytmu, nie ma pojęcia o szczegółach działania. Wszystko, co musi wiedzieć, to z kim ma do czynienia (czyli wspólny interfejs), a cała reszta „magii” dzieje się w środku. To tak, jakbyś zamówił pizzę przez aplikację – nie wiesz, czy zrobi ją Janek czy Kasia, ale na końcu dostajesz pizzę i to się liczy!

Dlaczego używamy wzorca Strategia?

Oddzielenie odpowiedzialności:

Każda strategia (czyli sposób działania, algorytm) siedzi w swojej własnej klasie, zamiast mieszać się z innymi. To jak mieć szuflady na różne rzeczy w kuchni – jedna na sztućce, druga na przyprawy. Dzięki temu, jak chcesz dodać nowy algorytm (np. nowy sposób płatności), to po prostu tworzysz nową szufladę, bez bałaganu w reszcie kodu. To właśnie zasada SRP z SOLID – każda klasa ma swoją jedną odpowiedzialność.

Łatwość rozbudowy:

Potrzebujesz nowego algorytmu? Nie ma problemu! Możesz go dodać bez grzebania w starym kodzie. To jak dodanie nowego dania do menu w restauracji – kucharze dalej robią swoje, a Ty tylko dorzucasz nowy przepis. To wspiera zasadę OCP – kod jest otwarty na rozbudowę, ale zamknięty na modyfikacje. Stary kod zostaje nienaruszony, a Ty dorzucasz tylko nowe elementy.

Wielokrotne użycie:

Jeśli masz jeden fajny algorytm, to możesz go używać w różnych miejscach, bez kopiowania kodu. To jak jeden przepis na pizzę, który mogą wykorzystywać różne pizzerie – wszędzie ta sama, pyszna pizza, ale bez potrzeby pisania przepisu od nowa!

Elastyczność i konfiguracja:

Strategię (czyli sposób działania) możesz zmienić w trakcie działania aplikacji, zależnie od sytuacji. To trochę jak zmienianie kanałów w telewizorze – w jednej chwili oglądasz sport, ale nagle zmieniasz na serial, nie przerywając działania telewizora. Aplikacja nie musi się wyłączać ani modyfikować – po prostu dostosowuje się do aktualnych potrzeb.

Co osiągamy, używając wzorca Strategia?

Zamienność algorytmów:

Możesz zmieniać sposób działania aplikacji w trakcie jej pracy, bez dotykania starego kodu. To jak wymiana opon w samochodzie bez zatrzymywania się – po prostu zakładasz nowe, a auto jedzie dalej. Masz różne algorytmy do wyboru i możesz dowolnie nimi żonglować, w zależności od potrzeby.

Modularność i elastyczność:

Każdy algorytm (czyli strategia) siedzi sobie w swojej osobnej „klatce”, z dala od reszty kodu. Dzięki temu łatwo możesz dodać nowe strategie lub zmienić stare, bez wpływania na całą aplikację. To trochę jak zmiana składników w Twojej ulubionej kanapce – możesz wymienić sałatę na ogórka, nie rozbierając całego posiłku na części!

Poprawa organizacji kodu:

Twój kod staje się bardziej schludny i zorganizowany, niczym szafa z idealnie poskładanymi ubraniami. Dzięki temu jest łatwiej go zrozumieć, utrzymać i rozwijać. Kiedy ktoś wejdzie do projektu, nie poczuje się jakby trafił na pole bitwy, tylko na dobrze zorganizowany warsztat. Kod jest czytelny, łatwy do ogarnięcia i w pełni pod kontrolą – bez chaosu.

Jak implementujemy wzorzec Strategia?

Tworzymy wspólny interfejs:

Na początku definiujemy interfejs, który będzie bazą dla wszystkich strategii. To jak stworzenie wspólnego przepisu na zupę, który mówi, jakie składniki powinny być, ale nie mówi, jak dokładnie je przygotować. W interfejsie ogłaszamy metodę, dzięki której kontekst będzie wiedział, jak uruchomić daną strategię. To pierwszy krok do tego, aby każda strategia mogła grać w tej samej drużynie!

Konkretne strategie:

Teraz każda strategia, czyli konkretna metoda działania, musi zaimplementować ten interfejs. To tak, jak każdy kucharz przygotowujący zupę według tego samego przepisu, ale każdy z nich dodaje coś od siebie – jeden doda więcej czosnku, inny przyprawi ziołami. Dzięki temu mamy różne wersje algorytmu, z których kontekst może korzystać, zależnie od sytuacji.

Kontekst i wywoływanie strategii:

Kontekst to klasa, która przechowuje odniesienie do jednej z konkretnych strategii. To jak szef kuchni, który nie musi wiedzieć, jak dokładnie gotować każdą zupę – po prostu wie, gdzie znaleźć odpowiedni przepis. Kiedy kontekst chce uruchomić algorytm, wywołuje metodę strategii. Co ważne, kontekst nie ma pojęcia, z jaką strategią ma do czynienia – dla niego to po prostu „zupa”, która smakuje dobrze!

Klient i zmiana strategii:

Na końcu, klient (czyli klasa, która korzysta z strategii) tworzy konkretny obiekt strategii i przekazuje go kontekstowi. Kontekst, niczym elastyczny kucharz, pozwala klientowi na zmianę strategii w trakcie działania programu. Możesz zacząć od zupy pomidorowej, a potem, w zależności od humoru, przełączyć się na zupę ogórkową. Kontekst pozwala klientom zamienić strategię w trakcie działania programu.

Przykład w C#: System płatności

W tym przykładzie używamy wzorca Strategii, aby stworzyć system płatności, w którym różne metody płatności traktujemy jako zamienne strategie.
Dzięki temu możemy łatwo zmieniać sposób, w jaki płacimy, bez zbytniego grzebania w kodzie.

  • Definicja wspólnego interfejsu:
    Najpierw tworzymy interfejs IPaymentStrategy. To jak stworzenie wspólnego przepisu na różne dania – mówi, co powinno być w każdym przepisie, ale nie określa, jak dokładnie je przygotować. Interfejs definiuje metodę Pay, która przyjmuje kwotę do zapłaty.
// Definicja wspólnego interfejsu dla strategii
public interface IPaymentStrategy
{
    void Pay(decimal amount);
}
  • Konkretne strategie:
    Teraz dodajemy konkretne metody płatności, które implementują nasz interfejs. Mamy trzy opcje:
    • Płatność gotówką: Kiedy chcesz, żeby transakcja była szybka jak błyskawica (i nie musisz się martwić o karty).
    • Płatność kartą kredytową: Idealna, gdy portfel ma zbyt wiele wirtualnych kart, a Ty lubisz podróżować z minimalnym obciążeniem.
    • Płatność przez PayPal: Dla tych, którzy cenią sobie wygodę i wolą trzymać pieniądze w sieci.
// Konkretna strategia dla płatności gotówką
public class CashPayment : IPaymentStrategy
{
    public void Pay(decimal amount)
    {
        Console.WriteLine($"Płacenie {amount} zł gotówką.");
    }
}

// Konkretna strategia dla płatności kartą kredytową
public class CreditCardPayment : IPaymentStrategy
{
    public void Pay(decimal amount)
    {
        Console.WriteLine($"Płacenie {amount} zł kartą kredytową.");
    }
}

// Konkretna strategia dla płatności PayPal
public class PayPalPayment : IPaymentStrategy
{
    public void Pay(decimal amount)
    {
        Console.WriteLine($"Płacenie {amount} zł przez PayPal.");
    }
}
  • Kontekst płatności:
    Klasa PaymentContext przechowuje wybraną strategię płatności. To jak szef kuchni, który trzyma w ręku przepis na danie, które zamierza przygotować.
    Metoda SetPaymentStrategy pozwala na ustawienie, jaką strategię chcemy wykorzystać,
    a MakePayment realizuje płatność zgodnie z wybraną metodą. Jeśli nie ustawisz strategii, dostaniesz komunikat „Nie ustawiono strategii płatności” – jakby szef kuchni powiedział: „Nie wiem, co gotować!”.
// Klasa kontekstowa, która używa strategii
public class PaymentContext
{
    private IPaymentStrategy _paymentStrategy;

    // Możliwość ustawienia strategii w dowolnym momencie
    public void SetPaymentStrategy(IPaymentStrategy paymentStrategy)
    {
        _paymentStrategy = paymentStrategy;
    }

    // Metoda do realizacji płatności
    public void MakePayment(decimal amount)
    {
        if (_paymentStrategy == null)
        {
            Console.WriteLine("Nie ustawiono strategii płatności.");
            return;
        }
        _paymentStrategy.Pay(amount);
    }
}
  1. Użycie wzorca Strategia w programie:
    W Main() tworzymy PaymentContext, który pozwala na łatwe przełączanie się między różnymi metodami płatności. Możemy zacząć od płatności gotówką, potem zmienić na kartę kredytową, a na końcu skorzystać z PayPal – jakbyśmy zmieniali dania w restauracji w zależności od tego, co mamy ochotę zjeść.
// Użycie wzorca Strategia
class Program
{
    static void Main(string[] args)
    {
        PaymentContext paymentContext = new PaymentContext();

        // Zmiana strategii na płatność gotówką
        paymentContext.SetPaymentStrategy(new CashPayment());
        paymentContext.MakePayment(500m);

        // Ustawienie strategii na płatność kartą kredytową
        paymentContext.SetPaymentStrategy(new CreditCardPayment());
        paymentContext.MakePayment(600m);

        // Zmiana strategii na płatność PayPal
        paymentContext.SetPaymentStrategy(new PayPalPayment());
        paymentContext.MakePayment(300m);
    }
}

Dzięki wzorcowi Strategii możemy elastycznie dostosowywać metody płatności w naszej aplikacji.
To tak, jakbyś miał wybór w restauracji, co zjeść na lunch – a każda decyzja sprawia, że ​​wszystko smakuje lepiej!

Podsumowanie

Wzorzec Strategia to jak magiczny klucz do zamienności algorytmów i logik w aplikacji. Dzięki niemu możemy zmieniać sposób działania programu bez potrzeby grzebania w jego kodzie. To tak, jakbyś miał uniwersalne narzędzie, które pasuje do różnych zamków – nie musisz martwić się o to, co się dzieje wewnątrz, po prostu używasz odpowiedniego klucza w odpowiedniej chwili.

Izolacja szczegółów implementacyjnych:
Dzięki wzorcowi możesz oddzielić szczegóły działania algorytmu od kodu, który go używa. To jak gotowanie w kuchni – masz przepis (interfejs), ale nie musisz znać wszystkich tajemnic kucharza, żeby zjeść smaczny posiłek. Zamiast tego, masz danie, które smakuje świetnie, a cała magia dzieje się w tle.

Modularność i łatwość w utrzymaniu:
Kod staje się bardziej modularny, jak dobrze zorganizowany warsztat – każda strategia to osobna szuflada, a Ty wiesz dokładnie, gdzie znaleźć narzędzia, których potrzebujesz. Dzięki temu łatwiej jest utrzymywać i rozwijać aplikację. Możesz wprowadzać nowe strategie (czyli metody działania) bez zmiany starego kodu. To jak dodawanie nowego przepisu do książki kucharskiej – nie musisz wyrzucać poprzednich, wystarczy, że dodasz nowy!

Szerokie zastosowanie:
Wzorzec ten jest idealny wszędzie tam, gdzie istnieje wiele sposobów na zrealizowanie zadania. Możesz go użyć w systemach płatności, gdzie użytkownicy mogą wybierać różne metody, w walidacji danych, czy w różnych podejściach do przetwarzania informacji. To jak szwedzki stół – każdy może wybrać, co lubi najbardziej, a Ty nie musisz martwić się o to, kto co wybiera!

Nie bój się eksperymentować z różnymi wzorcami projektowymi.
Każdy z nich ma swoje unikalne zalety i może pomóc Ci w stworzeniu lepszego, bardziej skalowalnego kodu.
Jak mawiają kucharze: „Nie ma złych przepisów, są tylko różne smaki!”

Zachęcam Cię do odkrywania innych wzorców, takich jak Mediator, Obserwator, Singleton czy Wzorzec Fabryka.
Kto wie, może znajdziesz swój ulubiony przepis, który odmieni Twój sposób programowania!

💡 Wolisz oglądać niż czytać? Sprawdź mój film na YouTube, który dokładnie omawia ten przykład!

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *