Potyczki na froncie, odcinek 4: Reaktywna ifologia z powtórzeniami


Wstęp

Niedawno musiałem dodać do Angularowego frontendu prosty komponent, który poza wyświetleniem okna z komunikatem miał do wykonania tylko jedno zadanie: zapytać backend, czy występuje nowy proces określonego typu, a w przypadku pozytywnej odpowiedzi pobrać identyfikator owego procesu. Jeśli wywołana metoda api zwróciła jako identyfikator pusty ciąg znaków, próba pobrania identyfikatora powinna być powtarzana co sekundę aż do skutku, tzn. do momentu zwrócenia niepustego ciągu znaków.

Zadanie rozwiązałem na dwa sposoby. Najpierw przedstawię ten najbardziej intuicyjny i najprostszy, a następnie ten bardziej właściwy, korzystający w pełni z filozofii programowania reaktywnego.

Rozwiązanie 1

Pierwsza z metod powinna sprawdzić, czy istnieje proces do pobrania i ewentualnie wywołać metodę, która zajmie się pobraniem identyfikatora. Mogłoby zatem wyglądać tak:

Potrzebujemy jeszcze metody, która będzie ponawiać próby pobrania identyfikatora procesu, co można zrealizować, stosując rekurencję z opóźnieniem generowanym metodą setTimeout:

Łącząc powyższe metody w całość i dodając wywołanie metody finishProcessing(), która może np. mieć za zadanie zakończenie wyświetlania ikony ładowania danych, otrzymujemy poniższy kod:

Powyższy kawałek kodu składa się z zaledwie dwóch niezbyt skomplikowanych metod i doskonale realizuje swoje zadanie, ale to nie znaczy, że jest doskonały. Przede wszystkim mamy tu zagnieżdżoną subskrypcję observabli, co może być postrzegane jako antywzorzec. Zadanie da się wykonać w sposób bardziej ‚reaktywny’, budując jeden pipeline zawierający całą pożądaną logikę. Przy okazji nasz kod stanie się też bardziej suchy (ang. DRY – zasada ‘Don’t Repeat Yourself’) i bardziej solidny (zgodny ze zbiorem zasad ‘SOLID’).

Jak zatem doprawić powyższy kod reaktywnymi przyprawami w taki sposób, aby bardziej smakował wszystkim fanom programowania funkcyjnego i nie tylko?

Rozwiązanie 2


Prosty pipeline sprawdzający istnienie procesu i pobierający jego id mógłby wyglądać tak:

Pierwszym wyzwaniem jest umieszczenie w pipeline instrukcji warunkowej inicjującej próby pobrania identyfikatora tylko wtedy, gdy ustaliliśmy, że proces istnieje. Sam RxJsowy pipeline nie przewiduje miejsca na warunkowe wywołanie lub pomijanie swoich poszczególnych etapów. Można jednak umieścić instrukcję warunkową na co najmniej dwa sposoby.


‘If’ możemy wstrzyknąć do środka jednej z metod tworzących pipeline, tak aby ta metoda zwracała określoną observable w zależności od określonych warunków.

W powyższym przykładzie próbujemy pobierać id procesu pod warunkiem, że ów proces istnieje. Nasza metoda musi się odpowiednio zachować, tzn. zwrócić observable odpowiedniego typu także wtedy, gdy proces nie istnieje. Korzystając z polecenia ‘of’ metoda zwraca wówczas konkretny komunikat o braku procesu.


Alternatywnie do powyższego sposobu możemy wykorzystać instrukcję iif, która zwraca jedną z dwóch observabli w zależności od wartości zwróconej przez predykat. Możemy zatem uzyskać identyczny efekt jak powyżej bez tworzenia  dodatkowego dedykowanego obiektu observable do obsługi logiki warunkowego wyboru zwracanego observable.
Po zastosowaniu iif pipleine wyglądałby następująco:

Drugi problem to warunkowe powtarzanie prób pobrania identyfikatora procesu. W RxJs mamy kilka metod umożliwiających powtarzanie wywołania danej Obsevable.

Metoda repeat powtarza określoną ilość razy subskrypcję observable, do którego pipe’a została dołączona. Robi to natychmiastowo zawsze wtedy, gdy observable nie zwrócił błędu.

Druga metoda – retry – działa analogicznie jak repeat, z tą jedyną różnicą, że ponawia subskrypcję zawsze wtedy i tylko wtedy, gdy observable zwróci dowolnego typu błąd.

Obie metody mają tę wadę, że nie pozwalają na warunkowe ponawianie subskrypcji i na zdefiniowanie określonych opóźnień pomiędzy próbami.

Dlatego wykorzystałem kolejną z metod – retryWhen. Jej użycie jest najbardziej skomplikowane, ale daje niemal nieograniczone możliwości zdefiniowania tzw. strategii ponawiania wywołania observable.

Omawiając zbiór poleceń umożliwiających ponawianie subskrypcji, warto wspomnieć jeszcze o repeatWhen oraz expand. Pierwsze z nich umożliwia powtarzanie wywołań zgodnie z określoną strategią, która nie ma dostępu do wartości wygenerowanych przy poprzedniej próbie i dlatego nie jest użyteczna w naszym przypadku. Metoda expand pozwala natomiast na rekurencyjne wywołanie observable’a, które można uzależnić od poprzednio zwróconej wartości. Teoretycznie można by ją więc wykorzystać w opisywanym scenariuszu. Nie zrobiłem tego jednak z powodu problemów ze zmuszeniem jej do działania zgodnego z teorią.

Metoda retryWhen, podobnie jak retry, ponawia wywołanie jedynie po zwróceniu błędu przez observable. W opisywanym przypadku takie działanie nie jest pożądane. Przeciwnie, nie chcemy ponawiać wywołań w przypadku wystąpienia błędu, a jedynie w przypadku zwrócenia pustej odpowiedzi. Ten problem nietrudno jednak rozwiązać. Jeśli brakuje nam błędu, możemy go wygenerować instrukcją throwError. Następnie wystarczy odpowiednio wykorzystać metodę retryWhen, aby powtórzyć wywołanie w przypadku wystąpienia określonego typu błędu. Finalne rozwiązanie sprowadza się do zwalidowania odpowiedzi polegającego na wygenerowaniu wyjątku w przypadku uzyskania niesatysfakcjonującej odpowiedzi. Następnie, wykorzystując retryWhen z odpowiednią strategią, powtarzamy wywołania w przypadku jednego ściśle określonego typu błędu.

Moja metoda walidująca generuje wyjątek, jeśli identyfikator jest pusty. Gdy identyfikator jest prawidłowy, czyli w tym przypadku po prostu niepusty, metoda przekazuje go dalej.

Strategię powtórzeń tworzymy, definiując obiekt zwracający funkcję pobierającą parametr w postaci Observable<any> i zwracającą notyfikację (dowolną wartość) wtedy, gdy subskrypcja ma zostać ponowiona lub generującą wyjątek, jeśli nie chcemy już ponawiać wywołań.

Przykładowa strategia może wyglądać następująco:

Jeśli chcielibyśmy uzależnić dalsze powtarzanie prób numeru aktualnej próby, informację o  numerze próby możemy pobrać w taki sam sposób, jak informację o błędzie (zmienna count w poniższym przykładzie):

Tak przygotowaną strategię możemy wstrzyknąć do metody retryWhen.Poniższy przykład zawiera całą logikę walidacji i ponawiania wywołań:

Łącząc ze sobą wszystkie powyższe wycinki kodu, całe zadanie można w sposób reaktywny rozwiązać tak jak na przykładzie poniżej:

Podsumowanie

Powyższa konstrukcja jest trochę bardziej złożona niż pierwszy, mniej reaktywny wariant i można spierać się o to, czy jest konieczna w takim prostym przypadku. Reaktywne rozwiązanie ma jednak tę niepodważalną zaletę, że pozwala na dalsze rozbudowywanie logiki we względnie bezpieczny sposób, nie pogarszający znacząco czytelności kodu i pozwalający na zminimalizowanie ryzyka błędów, które z kolei przy rozbudowie nieaktywnego kodu rosłoby wykładniczo.


Warto moim zdaniem trenować reaktywne programowanie, nawet w tak prostych przypadkach, jak ten powyższy, aby zdobyć doświadczenie niezbędne do panowania nad znacznie bardziej skomplikowanymi strukturami przepływu danych.


Piotr Zyśk

O Piotr Zyśk

Od 2019 roku jako programista fullstack w Atenie zgłębiam głównie technologie .Net i Angular. Wcześniej przez kilkanaście lat zajmowałem się szeroko pojętym wsparciem administracyjno-programistycznym w dla dużej sieci call-center. W wolnym czasie lubię wykorzystywać swój komputer na różne sposoby, np. jako studio do tworzenia muzyki lub urządzenie do symulacji rajdów i wyścigów samochodowych. Często również można spotkać mnie w podolsztyńskich lasach na rowerze, z aparatem fotograficznym.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *