Wzorzec projektowy strategia

Wzorzec projektowy strategia

@ Devanath

Wzorzec strategia (ang. strategy pattern) obok wzorca fabryki jest jednym z częściej używanych wzorców projektowych. Jest on łatwy zarówno w zrozumieniu jak i w implementacji. Należy on do wzorców czynnościowych czyli takich, które opisują pewne zachowanie.

Wzorzec ten jest zgodny z zasadą Open-closed principle (otwarte-zamknięte). Dzięki temu możemy dodać nowy kod nie zmieniając dotychczasowego.

Spójrzmy na opis wzorca oraz przykłady. W ten sposób dowiemy się kiedy używać wzorca strategii i w jaki sposób jest on zbudowany.

Zastosowanie

Bardzo często gdy piszemy kod programu staje się on coraz bardziej złożony. Szczególnie ma to miejsce jeśli w naszym kodzie mamy dużo instrukcji warunkowych. Pomiędzy warunkami tych instrukcji powstają bloki kodu. Im więcej takich bloków tym kod jest mniej czytelny. Sprawia to, że utrzymanie takiego kodu jest trudne.

Wzorzec strategia pozwala rozwiązać ten problem. Odbywa się to przez wydzielenie wspólnej abstrakcji dla wszystkich tych bloków. Tę abstrakcje opisuje się za pomocą interfejsu. Natomiast zawartość każdego z bloków to osobny algorytm. Każdy z nich trafia do osobnej klasy.

Klasa, która używa strategii to klasa kontekstu (ang. context). Taka klasa ma pole do którego przypisuje się aktualnie wybrany obiekt strategii. Można w ten sposób zmieniać obiekt strategii w trakcie działania programu.

Jak zaimplementować?

  1. Zidentyfikuj kontekst czyli klasę w której znajduje się kod z dużą ilością instrukcji warunkowych.
  2. Określ zachowanie które jest wspólne dla tworzonej strategii.
  3. Utwórz interfejs strategii. Będzie on reprezentować to zachowanie.
  4. Zaimplementuj w klasach konkretne wersje algorytmu. Każda klasa będzie implementować zdefiniowany wcześniej interfejs.
  5. Zmień główną klasę tak by mogła współpracować ze strategią.

Przykładowa implementacja

Rozważmy prosty przykład klasy, która posiada taką metodę format:

 private String format;
 // . . .
 
 public String format(String text) {
     if (format.equals("upperCase")) {
         return text.toUpperCase();
     } else if (format.equals("lowerCase")) {
         return text.toLowerCase();
     } else if (format.equals("capitalize")) {
         return text.substring(0, 1).toUpperCase() + text.substring(1);
     } else {
        . . .
     }
 }

Posiada ona jeden parametr on nazwie text, który jest łańcuchem znaków. Łańcuch ten jest odpowiednio formatowany w metodzie. Po tej operacji jest zwracany z metody. Powyższy kod zawiera trzy sposoby formatowania.

Z czasem ilość możliwych sposobów formatowania może przyrastać. Kod metody będzie stawał się coraz dłuższy. Będzie też mniej czytelny. Ponadto wraz z każdym nowym sposobem formatowania trzeba będzie go zmieniać. Spowoduje to, że naruszymy zasadę otwarte-zamknięte. Wzorzec strategia pomoże rozwiązać te problemy.

Rozwiązanie

Oto kroki jakie należy wykonać aby zmienić kod, tak by używał wzorca strategia.

Krok 1 - określamy wspólne zachowanie

W pierwszym kroku szukamy wspólnego zachowania. Jest nim formatowanie tekstu.

Krok 2 - tworzymy interfejs strategii

Tworzymy interfejs strategii o nazwie TextFormatterStrategy. Interfejs ten posiada metodę format(String text).

 package pl.javadeveloper.design.patterns.strategy;
 
 public interface TextFormatterStrategy {
     public String format(String text);
 }

Krok 3 - tworzymy wersje algorytmu w osobnych klasach

W kolejnym kroku przenosimy fragmenty instrukcji warunkowych do nowych klas. Każda z klas realizuje jedną z wersji algorytmu. Każda z nich implementuje interfejs TextFormatterStrategy. Poniżej znajdziemy kod każdej z klas.

LowerCaseFormatter

 package pl.javadeveloper.design.patterns.strategy;
 
 public class LowerCaseFormatter implements TextFormatterStrategy {
     public String format(String text) {
         return text.toLowerCase();
     }
 }

UpperCaseFormatter

 package pl.javadeveloper.design.patterns.strategy;
 
 public class UpperCaseFormatter implements TextFormatterStrategy {
     public String format(String text) {
         return text.toUpperCase();
     }
 }

CapitalizeFormatter

 package pl.javadeveloper.design.patterns.strategy;
 
 public class CapitalizeFormatter implements TextFormatterStrategy {
     public String format(String text) {
        return text.substring(0, 1).toUpperCase() + text.substring(1);
     }
 }

Krok 4 - umożliwiamy wybór obiektu klasy strategii w klasie kontekstu

Teraz zmieniamy główną klasę, która będzie używać strategii. Dodajemy pole, które będzie przechowywało odwołanie do interfejsu TextFormatterStrategy. Tworzymy też metodę, która umożliwia przypisanie do tego pola wybranej implementacji. W metodzie print(String text) wywołujemy metodę format() strategii. Metoda ta wypisuje sformatowany ciąg znaków.

 package pl.javadeveloper.design.patterns.strategy;
 
 public class FormatterContext {
     private TextFormatterStrategy strategy;
 
     public void set(TextFormatterStrategy strategy) {
         this.strategy = strategy;
     }
 
     public void print(String text) {
        String formattedText = strategy.format(text);
        System.out.println(formattedText);
     }
 }

Aby pokazać działanie wzorca strategia tworzymy klasę klienta. W klasie tej inicjujemy klasę kontekstu. Potem po kolei podstawiamy każdą z implementacji. Następnie sprawdzamy wynik działania programu.

 package pl.javadeveloper.design.patterns.strategy;
 
 public class Client {
     private static final String TEXT = "Strategy Design Pattern";
 
     public static void main(String[] args) {
         FormatterContext context = new FormatterContext();
 
         context.set(new CapitalizeFormatter());
         context.print(TEXT);
 
         context.set(new UpperCaseFormatter());
         context.print(TEXT);
 
         context.set(new LowerCaseFormatter());
         context.print(TEXT);
    }
 }

Jeśli uruchomimy powyższy program, otrzymamy takie wyniki:

 Strategy Design Pattern
 STRATEGY DESIGN PATTERN
 strategy design pattern

Przykłady wzorca strategia w JDK

Kilka przykładów wzorca projektowego strategia w języku Java można również odnaleźć w Java Development Kit. W tym celu należy pobrać kody źródłowe ze strony http://www.oracle.com. Następnie należy zapoznać się poniższymi klasami.

Inne przykłady wzorca strategia

Oto inne ciekawe przykłady wzorca strategii znalezione w sieci:

Diagram UML

Spójrzmy z jakich elementów zbudowany jest wzorzec projektowy strategia. Przedstawia to poniższy diagram klas w języku UML.

Wzorzec strategia - diagram klas w języku UML

Diagram zawiera następujących uczestników:

  • Context - główna klasa, która posiada referencję do obiektu konkretnej strategii i współpracuje z nim za pomocą interfejsu strategii. Jak widzimy w klasie Context znajduje się pole przechowujące obiekt strategii. Obiekt konkretnej strategii ustawiany jest za pomocą metody setStrategy.
  • Strategy - wspólny interfejs dla wszystkich konkretnych implementacji strategii.
  • ConcreteStrategy – jedna z implementacji określonej wersji algorytmu.

Zalety i wady

Zalety

  • redukcja lub usunięcie wyrażeń warunkowych
  • prosta struktura kodu po przeniesieniu odmian algorytmu do określonych klas
  • możliwość zmiany algorytmu w czasie działania programu (dzięki zastosowaniu kompozycji usuwającej powiązanie między algorytmem a miejscem jego użycia)
  • łatwe testowanie klasy klienta i klas strategii
  • łatwa analiza kodu, gdy mamy do czynienia z dużą ilością algorytmów

Wady

  • złożona konstrukcja kodu (więcej klas)
  • złożony sposób pobierania danych z klasy kontekstu

Ćwiczenie do wykonania

Poniższa klasa o nazwie Calculator posiada metodę calculate. Metoda ta wykonuje określoną operację na dwóch liczbach. Rodzaj operacji określony jest przez parametr operator. Obecnie możliwe do wykonania operacje to:

  • dodawanie,
  • odejmowanie,
  • mnożenie,
  • dzielenie.

Ćwiczenie polega zmianie poniższej klasy tak, aby wykorzystywała ona wzorzec strategia.

public class Calculator {

    public int calculate(int a, int b, String operator) {
        int result = 0;

        if ("add".equals(operator)) {
            result = a + b;
        } else if ("multiply".equals(operator)) {
            result = a * b;
        } else if ("divide".equals(operator)) {
            result = a / b;
        } else if ("subtract".equals(operator)) {
            result = a - b;
        }
        return result;
    }
}

Materiały dodatkowe

Książki

Bardziej szczegółowe informacje na temat wzorca strategia możesz znaleźć w następujących książkach:

  1. Wzorce projektowe. Rusz głową! Autorzy: Eric Freeman, Bert Bates, Kathy Sierra, Elisabeth Robson.
  2. Wzorce projektowe. Elementy oprogramowania obiektowego wielokrotnego użytku. Autorzy: Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides.
  3. Refaktoryzacja do wzorców projektowych. Autor: Joshua Kerievsky.

Materiały wideo

Derek Banas przedstawia wzorzec strategii w poniższym filmie.

Podsumowanie

Wzorce projektowe pomogają rozwiązywać problemy pojawiające się podczas tworzenia kodu. Z powyższych rozważań wynika, że wzorzec projektowy strategii pozwala nam pisać bardziej elastyczny kod. Możemy dodać do systemu nowe zachowanie bez zmian w istniejącym kodzie. Przeniesienie konkretnych wersji algorytmu do osobnych klas sprawia, że kod staje się bardziej czytelny. Dzięki temu, że kod algorytmów znajduje się w osobnych klasach łatwiej też jest napisać do nich testy.

Kody źródłowe opisane w tym artykule znajdują się na GitHub-ie.

Przeczytaj także