Postem tym chciałbym rozpocząć mini cykl o zastosowaniu wzorców projektowych w języku JavaScript - wbrew pozorom, w tym języku również można je stosować! Na pierwszy ogień przedstawię dziś wzorzec Strategia w JavaScript. Myślę, że większość z nas, stosowała i stosuje ten wzorzec na co dzień (czasem nawet nieświadomie), jednak dla przypomnienia (za wikipedią):

Wzorzec strategii definiuje rodzinę algorytmów, z których każdy zdefiniowany jest w osobnej klasie implementującej wspólny interfejs - dzięki temu możliwe jest wymienne stosowanie każdego z tych algorytmów, nie będąc zależnymi od klas klienckich.

Przykład - HTML

To tyle tytułem wstępu - zakładam, że implementacja tego wzorca w języku takim jak np. C# jest znana, chciałbym się więc skupić na tym jak skorzystać z niego JavaScript. Dla osób, które nie miały dotąd styczności z OOP w JavaScript, proponuje wcześniejsze zapoznanie się z tą tematyką. Można zacząć na przykład od stron mozilli dla developerów. Przejdźmy zatem do przykładu - załóżmy taki oto kod html:

<fieldset>
  <label for="error">Błąd</label>
  <input type="radio" name="selection" value="error" />

  <label for="warnirng">Ostrzeżenie</label>
  <input type="radio" name="selection" value="warning" />

  <label for="info">Informacja</label>
  <input type="radio" name="selection" value="info" /><
</fieldset>
<input type="button" value="Wyswietl" />

Mamy tutaj trzy radiobuttony, przycisk_‘Wyświetl’_ oraz paragraf. Radiobuttony odpowiadają opcjom_‘Błąd’,‘Ostrzeżenie’_ oraz ‘Informacja’ - po wybraniu jednego z nich i naciśnięciu przycisku ‘Wyświetl’, wewnątrz tag’u <p>, zamiast wartości ‘—’ ma się pojawić odpowiednia informacja (dodatkowo w zależności czy będzie to błąd, ostrzeżenie czy informacja, tekst powinien być odpowiednio “ostylowany”).

Obsługa zadania

Do obsługi takiego zadania moglibyśmy użyć takiego kodu JavaScript:

$(document).ready(function() {
    $('input:radio').change(function() {
        var checkedVal = $(this).val();

        switch (checkedVal) {
            case 'error':
                $('input[type="button"]').click(function() {
                    $('p').attr('class', '')
                        .addClass('error')
                        .text('Błąd');
                });
                break;
             case 'warning':
                $('input[type="button"]').click(function() {
                    $('p').attr('class', '')
                        .addClass('warning')
                        .text('Ostrzeżenie');
                });
                break;
            case 'info':
                $('input[type="button"]').click(function() {
                    $('p').attr('class', '')
                        .addClass('info')
                        .text('Informacja');
                });
                break;
        }
    });
});

Jak widać, w “on document ready” (jQuery) mamy inicjalizację obsługi zdarzenia change dla wszystkich radiobuttonów na stronie. Funkcja obsługująca zdarzenie pobiera wartość atrybutu value radiobuttona, który został w danym momencie kliknięty - w zależności od tej wartości, definiowana jest funkcja obsługująca zdarzenie click przycisku (tego, który wyświetlać ma komunikat). W powyższym przykładzie, zastosowana została instrukcja warunkowa switch, a każdy z obsługiwanych przypadków jest bardzo podobny - mamy funkcję obsługującą zdarzenie click, a w niej, z pomocą metod jQuery, odpowiednie “ostylowanie” elementu <p> i nadanie odpowiedniego tekstu.

Myślę, że w takim przypadku, aż się prosi o zastosowanie wzorca strategii… W naszym przypadku, utworzymy klasę abstrakcyjną zawierającą deklarację metody show(), która odpowiedzialna będzie za odpowiednie “ostylowanie” naszego elementu <p\> - odpowiednia implementacja tej metody będzie wywoływana w funkcji obsługującej zdarzenie click buttona Wyświetl.

Implementacja strategii

Zacznijmy więc przykład implementacji strategii, na początek klasa bazowa:

var Message = function() {}; // define abstract class
// declare abstract function
Message.prototype.show = function() {
    throw 'Implement abstract class!!';
};

Mamy tutaj definicję klasy Message, z konstruktorem bez parametrów (function() nie przyjmuje żadnych parametrów). Ponadto definiujemy metodę show(), która rzuca wyjątkiem jeśli próbowalibyśmy użyć jej bezpośrednio (czyli jest to metoda bardziej wirtualna niż abstrakcyjna - w JavaScript nie mamy takiego rozróżnienia, liczy się efekt jaki w ten sposób osiągamy - jeśli w klasie dziedziczącej po Message nie zaimplementujemy metody show(), rzucony zostanie wyjątek).

Klasy dziedziczące po Message

Kolejna rzecz, to implementacja poszczególnych klas strategii dziedziczących po Message. Poniżej jedna z takich klas:

var ErrorMessage = function() { /*parameterless constructor*/ };
ErrorMessage.prototype = new Message; // extend of abstract class
ErrorMessage.prototype.show = function() {
    // first algorithm implementation
    $('p').attr('class', '')
        .addClass('error')
        .text('Błąd!');
};

Na powyższym przykładzie, widać w jaki sposób realizowane jest dziedziczenie w JavaScript - w pierwszej linijce mamy deklarację klasy ErrorMessage za pomocą konstruktora bezparametrowego. W kolejnej linii informujemy, że klasa ErrorMessage dziedziczy z Message (klasa Message jest prototypem dla ErrorMessage). Kolejne linijki to już konkretna implementacja/przesłonięcie metody show(). Poniżej pozostałe strategie:

var WarningMessage = function() { /*parameterless constructor*/ };
WarningMessage.prototype = new Message; // extend of abstract class
WarningMessage.prototype.show = function() {
    // first algorithm implementation
    $('p').attr('class', '')
        .addClass('warning')
        .text('Ostrzeżenie');
};

var InfoMessage = function() { /*parameterless constructor*/ };
InfoMessage.prototype = new Message; // extend of abstract class
InfoMessage.prototype.show = function() {
    // first algorithm implementation
    $('p').attr('class', '')
        .addClass('info')
        .text('Informacja');
};

Wykorzystanie strategii w praktyce

Wzorzec strategii, w codziennym zastosowaniu bardzo często występuje w połączeniu z wzorcem fabryki - np. klasy ze statycznymi metodami zwracającymi odpowiednie implementacje algorytmu. W naszym przypadku zrobimy coś podobnego - zdefiniujemy obiekt, z właściwościami, których nazwy odpowiadać będą wartościom (atrybuty value) poszczególnych radiobuttonów, a wartości przypisanym im strategiom:

var messages = {
    error: new ErrorMessage(),
    warning: new WarningMessage(),
    info: new InfoMessage()
};

Pozostało nam już tylko wykorzystanie tak utworzonych strategii w praktyce:

$(document).ready(function() {
    $('input:radio').change(function() {
        currentMessage = messages[$(this).val()];
        $('input[type="button"]').click(currentMessage.show);
    });
});

Tak jak w przykładzie na początku artykułu, mamy obsługę zdarzenia change radiobuttonów, z tą różnicą, że zamiast instrukcji warunkowej switch wykorzystujemy utworzone wcześniej strategie - jak widać w linii nr 3, na podstawie wartości aktualnie zaznaczonego radiobuttona, z obiektu messages, pobierana jest odpowiednia klasa strategii. Następnie metoda show() tej klasy, wykorzystywana jest do obsługi zdarzenia click przycisku Wyświetl.

W ten sposób uniezależniliśmy wyświetlanie komunikatów od funkcji obsługującej zdarzenie click - jeśli chcemy dokonać zmian w sposobie wyświetlania, robimy to w odpowiednich klasach strategii. Możemy również napisać nowe strategie wyświetlania komunikatów i podmienić je - wszystkie korzyści płynące z wzorca strategia są dostępne.

Wzorzec strategia w JavaScript - podsumowanie

To w zasadzie wszystko, jak widać skorzystanie z benefitów wzorców projektowych jest możliwe również w języku JavaScript. Mam nadzieję, że teraz wzorzec strategia w JavaScript nie będzie już dla Ciebie tajemnicą… W przyszłości postaram się pokazać również implementację innych wzorców, znanych nam z typowych języków obiektowych.

W pełni działający opisany powyżej dostępny jest do przetestowania w jsfiddle.