Wiosenna WYPRZEDAŻ

Strategia

Znany też jako: Strategy

Cel

Strategia to behawioralny wzorzec projektowy pozwalający zdefiniować rodzinę algorytmów, umieścić je w osobnych klasach i uczynić obiekty tych klas wymienialnymi.

Wzorzec projektowy Strategia

Problem

Pewnego dnia postanawiasz stworzyć aplikację służącą nawigacji dla turystów. Aplikacja jest zbudowana w oparciu o piękną mapę ułatwiającą użytkownikom szybko zorientować się w topografii miasta.

Jedną z funkcji, o którą użytkownicy prosili najczęściej, było automatyczne planowanie trasy przez aplikację. Po wprowadzeniu adresu, na mapie pokazałaby się najszybsza trasa do tego miejsca.

Pierwsza wersja aplikacji potrafiła planować trasy po drogach. Osoby podróżujące autami były więc wniebowzięte. Okazało się jednak, że nie wszyscy lubią jeździć na urlop autem. Dlatego kolejna aktualizacja wprowadziła możliwość generowania szlaków pieszych. Niedługo po tym aplikacja zyskała opcję wyznaczania tras w oparciu o komunikację miejską.

To był jednak dopiero początek. W planach pojawiło się bowiem dodanie obsługi tras dogodnych dla rowerzystów. A jeszcze później zaplanowano tworzenie tras uwzględniających wszystkie atrakcje turystyczne miasta.

Kod nawigacji stał się bardzo zagmatwany.

Kod nawigacji stał się zagmatwany.

Chociaż z perspektywy biznesowej aplikacja odniosła sukces, aspekty techniczne stały się dla ciebie zmorą. Po każdym dodaniu kolejnego algorytmu wytyczania trasy, główna klasa nawigatora dwukrotnie się powiększała. W pewnym momencie okiełznanie tej bestii stało się zbyt trudne.

Każda zmiana któregoś z algorytmów — usunięcie usterki, czy też dostrajanie punktacji kolejnych odcinków trasy wpływało na całą klasę, zwiększając tym samym ryzyko błędu w już działającym kodzie.

Na dodatek ucierpiała praca zespołowa. Twoi współpracownicy, zatrudnieni po udanym wprowadzeniu programu na rynek, zaczęli narzekać, że marnują zbyt wiele czasu na rozwiązywanie konfliktów scalania. Implementacja każdej nowej funkcji wymaga zmiany tej samej rozbudowanej klasy, nad którą jednocześnie pracują inni.

Rozwiązanie

Wzorzec Strategia proponuje ekstrakcję poszczególnych algorytmów wykonujących dane zadanie na różne sposoby i umieszczenie ich w odrębnych klasach, zwanych strategiami.

Pierwotna klasa, zwana kontekstem, musi zawierać pole służące przechowywaniu odniesienia do którejś ze strategii. Kontekst deleguje pracę powiązanemu obiektowi typu strategia, zamiast wykonywać ją samodzielnie.

Kontekst nie jest odpowiedzialny za wybór stosownego algorytmu dla danego zadania. To klient przekazuje żądaną strategię kontekstowi. Co więcej, kontekst nie wie zbyt wiele o strategiach. Współpracuje ze wszystkimi strategiami za pośrednictwem tego samego, ogólnego interfejsu, który eksponuje pojedynczą metodę uruchamiającą algorytm ukryty w danej strategii.

Tym sposobem kontekst staje się niezależny od konkretnych strategii, więc można dodawać kolejne algorytmy, lub modyfikować istniejące, bez zmieniania kodu kontekstu lub kodu innych strategii.

Strategie planowania trasy

Strategie planowania trasy.

W naszej aplikacji nawigacyjnej, każdy algorytm wyznaczania trasy może zostać wyekstrahowany do swojej własnej, odrębnej klasy, posiadającej jedną metodę stwórzTrasę. Metoda przyjmuje informacje o punkcie początkowym i docelowym, a zwraca zestaw kluczowych etapów wyznaczonej trasy.

Każda klasa wyznaczająca trasę może wytyczyć inną trasę w oparciu o te same argumenty, a główna klasa nawigator nie musi wiedzieć o obranym algorytmie, gdyż zajmuje się jedynie przedstawianiem trasy na mapie. Klasa posiada metodę umożliwiającą zmianę aktywnej strategii planowania trasy, dzięki czemu jej klienci, jak na przykład przyciski interfejsu użytkownika, mogą zmienić bieżący sposób wyznaczania trasy na inny.

Analogia do prawdziwego życia

Różne strategie przewozu

Różne strategie dotarcia na lotnisko.

Wyobraź sobie, że musisz dotrzeć na lotnisko. Możesz złapać autobus, zamówić taksówkę lub pojechać rowerem. To są twoje strategie przejazdu. Możesz wybrać jedną z nich zależnie od czynników takich jak budżet lub ograniczenia czasowe.

Struktura

Struktura wzorca StrategiaStruktura wzorca Strategia
  1. Kontekst przechowuje odniesienie do jednej z konkretnych strategii i komunikuje się z jej obiektem za pośrednictwem interfejsu strategia.

  2. Interfejs Strategia jest wspólny dla wszystkich konkretnych strategii. Deklaruje metodę za pomocą której kontekst uruchamia daną strategię.

  3. Konkretne Strategie implementują różne warianty algorytmu z którego korzysta kontekst.

  4. Kontekst wywołuje metodę uruchamiającą eksponowaną przez powiązany z nim obiekt strategii za każdym razem gdy chce uruchomić algorytm. Kontekst nie wie z jaką strategią ma do czynienia, lub jak działa algorytm.

  5. Klient tworzy określony obiekt strategii i przekazuje go kontekstowi. Kontekst eksponuje metodę setter pozwalającą klientom zamienić strategię skojarzoną z tym kontekstem w trakcie działania programu.

Pseudokod

W poniższym przykładzie, kontekst wykorzystuje kilka strategii by wykonać różne działania arytmetyczne.

// Interfejs strategii deklaruje działania wspólne dla
// wszystkich wspieranych wersji jakiegoś algorytmu. Kontekst
// korzysta z tego interfejsu w celu wywoływania algorytmów
// zdefiniowanych przez konkretne strategie.
interface Strategy is
    method execute(a, b)

// Konkretne strategie implementują algorytm według poniższego
// interfejsu bazowej strategii. Interfejs sprawia, że są one
// wymienialne w kontekście.
class ConcreteStrategyAdd implements Strategy is
    method execute(a, b) is
        return a + b

class ConcreteStrategySubtract implements Strategy is
    method execute(a, b) is
        return a - b

class ConcreteStrategyMultiply implements Strategy is
    method execute(a, b) is
        return a * b

// Kontekst definiuje interfejs stanowiący przedmiot
// zainteresowania klientów.
class Context is
    // Kontekst posiada odniesienie do jednego z obiektów-
    // strategii. Kontekst nie zna konkretnej klasy strategii.
    // Powinien współpracować ze wszystkimi strategiami za
    // pośrednictwem wspólnego interfejsu strategii.
    private strategy: Strategy

    // Zazwyczaj kontekst przyjmuje strategię poprzez
    // konstruktor. Udostępnia także funkcję setter
    // umożliwiającą wymianę strategii na inną w trakcie
    // działania programu.
    method setStrategy(Strategy strategy) is
        this.strategy = strategy

    // Kontekst deleguje jakąś pracę obiektowi-strategii zamiast
    // samodzielnie implementować wiele wersji algorytmu.
    method executeStrategy(int a, int b) is
        return strategy.execute(a, b)


// Kod klienta wybiera jakąś konkretną strategię i przekazuje ją
// kontekstowi. Klient powinien być świadom różnic pomiędzy
// poszczególnymi strategiami aby móc podjąć właściwy wybór.
class ExampleApplication is
    method main() is
        Create context object.

        Read first number.
        Read last number.
        Read the desired action from user input.

        if (action == addition) then
            context.setStrategy(new ConcreteStrategyAdd())

        if (action == subtraction) then
            context.setStrategy(new ConcreteStrategySubtract())

        if (action == multiplication) then
            context.setStrategy(new ConcreteStrategyMultiply())

        result = context.executeStrategy(First number, Second number)

        Print result.

Zastosowanie

Stosuj wzorzec Strategia gdy chcesz używać różnych wariantów jednego algorytmu w obrębie obiektu i zyskać możliwość zmiany wyboru wariantu w trakcie działania programu.

Wzorzec Strategia pozwala pośrednio zmienić zachowanie obiektu w trakcie działania programu poprzez przypisywanie temu obiektowi różnych podobiektów wykonujących określone poddziałania na różne sposoby.

Warto stosować ten wzorzec gdy masz w programie wiele podobnych klas, różniących się jedynie sposobem wykonywania jakichś zadań.

Wzorzec Strategia umożliwia ekstrakcję różniących się zachowań do odrębnej hierarchii klas i połączenie pierwotnych klas w jedną, redukując tym samym powtórzenia kodu.

Strategia pozwala odizolować logikę biznesową klasy od szczegółów implementacyjnych algorytmów, które nie są istotne w kontekście tej logiki.

Strategia umożliwia odizolowanie kodu różnych algorytmów, ich danych wewnętrznych oraz zależności od reszty kodu. Klienci otrzymują prosty interfejs umożliwiający uruchamianie algorytmów i wymiany ich na inne w trakcie działania programu.

Stosuj ten wzorzec gdy twoja klasa zawiera duży operator warunkowy, którego zadaniem jest wybór odpowiedniego wariantu tego samego algorytmu.

Wzorzec Strategia pozwala pozbyć się wyżej wymienionych kawałków kodu poprzez ekstrakcję algorytmów do odrębnych klas implementujących taki sam interfejs. Pierwotny obiekt deleguje uruchamianie jednemu z tych obiektów, zamiast samodzielnie implementować wszystkie warianty algorytmu.

Jak zaimplementować

  1. W klasie kontekstu zidentyfikuj algorytm który często może ulegać zmianom. Może to być też obszerna instrukcja warunkowa wybierająca i uruchamiająca wariant tego samego algorytmu w trakcie działania programu.

  2. Zadeklaruj interfejs strategii który będzie wspólny dla wszystkich wariantów algorytmu.

  3. Jeden po drugim ekstrahuj wszystkie algorytmy do odrębnych klas implementujących interfejs strategii.

  4. W klasie kontekstu, dodaj pole służące przechowywaniu odniesienia do obiektu strategii. Przygotuj metodę setter służącą zmianie wartości tego pola. Kontekst powinien współpracować z obiektem strategii wyłącznie przez interfejs strategii. Kontekst może definiować interfejs dający strategii dostęp do swoich danych.

  5. Klienci kontekstu muszą skojarzyć go ze stosowną strategią, która odpowiada sposobowi w jaki kontekst ma wykonać swoje zadanie.

Zalety i wady

  • Możesz zamieniać algorytmy stosowane w obrębie obiektu w trakcie działania programu.
  • Możesz odizolować szczegóły implementacyjne algorytmu od kodu który z niego korzysta.
  • Umożliwia zamianę dziedziczenia na kompozycję.
  • Zasada otwarte/zamknięte. Możliwe jest wprowadzanie nowych strategii bez konieczności dokonywania zmian w kontekście.
  • Jeśli masz zaledwie kilka algorytmów i rzadko ulegają one zmianie, nie ma wyraźnej potrzeby nadmiernego komplikowania programu przez dodawanie nowych klas i interfejsów związanych z tym wzorcem.
  • Klienci muszą być świadomi różnic pomiędzy poszczególnymi strategiami, aby mogli wybrać właściwą.
  • Wiele nowoczesnych języków programowania posiada wsparcie dla typów funkcyjnych pozwalających zaimplementować różne wersje algorytmu wewnątrz zestawu anonimowych funkcji. Można następnie korzystać z tych funkcji dokładnie tak jak z obiektów strategia, ale bez konieczności rozbudowy kodu o kolejne klasy i interfejsy.

Powiązania z innymi wzorcami

  • Most, Stan, Strategia (i w pewnym stopniu Adapter) mają podobną strukturę. Wszystkie oparte są na kompozycji, co oznacza delegowanie zadań innym obiektom. Jednak każdy z tych wzorców rozwiązuje inne problemy. Wzorzec nie jest bowiem tylko receptą na ustrukturyzowanie kodu w pewien sposób, lecz także informacją dla innych deweloperów o charakterze rozwiązywanego problemu.

  • Polecenie i Strategia mogą wydawać się podobne, ponieważ oba mogą służyć parametryzacji obiektu jakimś działaniem. Mają jednak inne cele.

    • Za pomocą Polecenia można konwertować dowolne działanie na obiekt. Parametry działania stają się polami tego obiektu. Konwersja zaś pozwala odroczyć wykonanie działania, kolejkować je i przechowywać historię wykonanych działań, a także wysyłać polecenia zdalnym usługom, itd.

    • Z drugiej strony, Strategia zazwyczaj opisuje różne sposoby wykonywania danej czynności, pozwalając zamieniać algorytmy w ramach jednej klasy kontekstu.

  • Dekorator pozwala zmienić otoczkę obiektu, zaś Strategia jej wnętrze.

  • Metoda szablonowa polega na mechanizmie dziedziczenia: pozwala zmieniać części algorytmu rozszerzając je w podklasach. Strategia bazuje na kompozycji: można zmienić część zachowania obiektu poprzez nadanie mu różnych strategii odpowiadających temu zachowaniu. Metoda szablonowa działa na poziomie klasy, więc jest statyczna. Strategia działa na poziomie obiektu, więc pozwala przełączać zachowania w trakcie działania programu.

  • Stan można uważać za rozszerzenie Strategii. Oba wzorce oparte są o kompozycję: zmieniają zachowanie kontekstu przez delegowanie części zadań obiektom pomocniczym. Strategia czyni te obiekty całkowicie niezależnymi i nieświadomymi siebie nawzajem. Jednakże Stan nie ogranicza zależności pomiędzy konkretnymi stanami i pozwala im zmieniać stan kontekstu według uznania.

Przykłady kodu

Strategia w języku C# Strategia w języku C++ Strategia w języku Go Strategia w języku Java Strategia w języku PHP Strategia w języku Python Strategia w języku Ruby Strategia w języku Rust Strategia w języku Swift Strategia w języku TypeScript