Wzorzec Deferred / Promise i przykłady dobrego zastosowania

Programowanie

Tytułowy wzorzec istnieje w zasadzie w powszechnym użytku od kiedy wyszła produkcyjna wersja jednej z najpopularniejszych bibliotek JavaScript, czyli jQuery w wersji 1.5, czyli mniej więcej od roku 2011. Niestety mimo tylu lat, wciąż bardzo rzadko można go spotkać w kodzie pisanym przez deweloperów tego języka mimo że sam wzorzec stosują oni nieświadomie w zasadzie w codziennej swojej pracy.


Warto sobie uświadomić, iż na wzorcu tym oparta jest spora część biblioteki jQuery, a czołowym obiektem implementującym go w praktyce jest $.ajax czy choćby animacje. Twórcy wiedzieli doskonale, że bez tego wzorca nie doprowadzą do znaczących postępów w rozwoju biblioteki, więc przyjęli specyfikację swojego rozwiązania bazującą na CommonJS Promises/A.

Dlatego też na początku napomniałem, iż deweloperzy bardzo często używają go czy tego chcą czy nie, lecz nieświadomie. Zastanawiające jest czemu brakuje owego "świadomego" użycia tej bardzo dobrej metodyki. Spróbuję przybliżyć w praktyce z czym to się je, oraz w jakich przypadkach warto stosować. Ktoś powie, czemu o tym pisze? Czyżbym też obudził się kilka lat po fakcie? Otóż nie, po prostu denerwuje fakt, iż praktycznie się nie spotyka w "codziennej" pracy jednej z najlepszych metodyk asynchronicznego i łańcuchowego wywołania wielu funkcji. Tym bardziej iż stało się to już standardem w JavaScript ES6.

 

Przykład teoretyczny

 

Jako że wzorzec ten niejako wyszedł z jQuery oraz z racji samej popularności bazujmy na przykładzie napisanym za pomocą właśnie tej biblioteki. Klasyczny przykład bardzo pięknie pokazujący główną idee.

function test1() {
    // tworzymy obiekt deferred (odroczenia)
    var deferred = $.Deferred()
    setTimeout(function() {
        // kiedy zadanie skończone oznaczamy promise (obietnice) jako wypełnioną
        deferred.resolve(1);
    }, 2000);
    // zwracamy obiekt promise
    return deferred.promise();
}

function test2() { 
    // tworzymy obiekt deferred (odroczenia)
    var deferred = $.Deferred()
    setTimeout(function() {
        // kiedy zadanie skończone oznaczamy promise (obietnice) jako wypełnioną
        deferred.resolve(2);
    }, 1000); 
    // zwracamy obiekt promise
    return deferred.promise();
}

var promise1 = test1();
var promise2 = test2();

$.when(promise1, promise2).then(function(a, b) {
    console.log("test1 i test2 wypełnione", a, b);
});

Jak zastosujemy powyższy przykład w praktyce okaże się że:

  • stworzyliśmy 2 funkcje o różnych czasach wykonania
  • uruchomiliśmy je w sposób równoległy i asynchroniczny
  • uzyskaliśmy efekt wykonania obu funkcji synchronicznie w jednej funkcji zwrotnej

A gdy zamienimy wywołanie tych funkcji na sposób jak poniżej:

    $.when(test1()).then(function (a) {
        console.log('test1 wykonany', a)
        return test2();
    }).then(function (b) {
        console.log("test2 wykonany", b);
    });

Okaże się że uzyskaliśmy łańcuchowe wywołanie funkcji gdzie wywołanie kolejnej uzależnione jest od poprzedniej. Obiekt $.Deferred (ang. odroczenie) służy do rejestrowania wielu wywołań zwrotnych i ich kolejkowania, przy czym każda z nich może przekazać swój stan - sukces bądź porażkę. Służą do tego najczęściej resolve oraz reject. Natomiast obiekt Promise (ang. obietnica) jest praktycznie takim samym obiektem jak Deffered z tą różnicą, że posiada jedynie  funkcje odpowiedzialne za rejestrowanie funkcji zwrotnych i nie ma możliwości kontroli ich stanu.

 

Praktyczny przykład użycia - preloading

 

    
    var promises = [];
    for (var i = 0; i < urls.length; i++) {
        (function(url, promise) {
            var img = new Image();
            img.onload = function() {
                promise.resolve();
            };
            img.src = url;
        })(urls[i], promises[i] = $.Deferred());
    }
    
    $.when.apply($, promises).done(function() {
        // gallery preloaded!
    });

Jest to jeden z idealnych zastosowań opisywanego wzorca. Jedyną nowością która może wywołać zakłopotanie u początkujących programistów to konstrukcja: $.when.apply($, promises). Apply służy do wywołania funkcji z tablicą argumentów. Pierwszy argument to kontekst, w tym wypadku $, czyli kontekst jQuery, natomiast drugi argument to tablica obiektów typu Promise. $.when przyjmuje nieskończoną liczbę argumentów w sposób definiowany liniowo, więc musimy przekształcić naszą tablicę argumentów za pomocą apply, która ją "spłaszczy" dla wywołania $.when. Ot cała filozofia.

 

Podsumowanie, a więc kiedy używać?

 

Zawsze warto spróbować dokonać refaktoryzacji, aby uczynić kod czytelniejszym, wydajniejszym, po prostu lepszym. Aczkolwiek wydaje się, że wzorzec Deferred / Promise ma szczególnie zastosowanie w sytuacjach:

  • kiedy mamy jedno lub wiele zadań które muszą być wykonane asynchronicznie np. czasochłonne obliczenia
  • kiedy mamy funkcje zwrotne zależne od rezultatów innych funkcji, czyli łańcuchowość zadań
  • podczas asynchronicznego ładowania wszelakiego typu zasobów tzw. assets
  • podczas pojedynczych, równoległych lub łańcuchowych żądań do serwera
  • podczas wykonywania złożonych animacji z różnymi zakresami kontekstów
Opublikowano Marzec 2017