Miesięcznik informatyków i menedżerów IT sektora publicznego

Paweł Ziółkowski

Technika programowania reaktywnego

Reagowanie na przyszłe zdarzenia – tak można najkrócej opisać technikę programowania reaktywnego, która od jakiegoś czasu zyskuje dużą popularność praktycznie w każdym języku. Zapoczątkował ją Microsoft, udostępniając na platformie .NET bibliotekę Rx.

Obecnie w naszym otoczeniu pojawia się ogromna ilość danych, które są przetwarzane przez aplikacje zarówno na komputerach, jak i urządzeniach mobilnych. Od tych aplikacji wymagamy, aby się nie zawieszały, jak najszybciej pokazywały wyniki działań użytkowników i żeby reagowały w odpowiednim czasie na interakcje z interfejsem. Problem może się pojawić, gdy aplikacja musi wyświetlić wiele informacji w jednym momencie, zawiera błędy w kodzie lub gdy występuje problem techniczny po stronie serwerów czy infrastruktury sieciowej.

Aby sprostać wymienionym problemom, został przedstawiony nowy sposób (paradygmat) pisania programów, który opiera się na opisaniu przez programistę, co chce osiągnąć, a nie, w jaki sposób – tak jak miało to miejsce dotychczas. Do tego zostały dołączone funkcje, których wynik działania zawsze jest przewidywalny i nie powoduje skutków ubocznych w programie. Pojawił się też nowy typ danych zwany strumieniem. Technika takiego programowania nazywa się programowaniem reaktywnym (reactive programming).

Asynchroniczność

Starsze programy komputerowe miały za zadanie wykonać kroki prowadzące do określonego wyniku, który był przedstawiany użytkownikowi. Po uruchomieniu programu użytkownik nie musiał wykonywać dodatkowych działań w interfejsie, lecz jedynie oczekiwał na zakończenie programu. Ponadto mógł obserwować w terminalu wyniki kolejnych kroków. Tak napisany program jest synchroniczny, czyli każda następna linijka kodu jest wykonana tylko wtedy, gdy poprzednia została ukończona. Taki przykładowy kod aplikacji w języku JavaScript wygląda następująco:

let zmienna = 8;
obliczWynik();
let dane = PobierzDane();
kolejneObliczenia(dane);

Kod zostanie wykonany linijka po linijce. W sytuacji gdy wykonanie funkcji obliczWynik czy PobierzDane wymaga dużo czasu, program będzie oczekiwał na ich zakończenie, zanim wywoła kolejny kod. Jeżeli będzie to aplikacja z interfejsem użytkownika, program nie odpowie na kliknięcia w przycisk czy wpisywanie danych w pole tekstowe. Wątek, w którym wykonywany jest kod wspomnianych funkcji oraz obsługi kliknięcia przycisku, jest ten sam, a przedstawiona akcja blokuje działanie programu. Dodatkowo marnuje się czas procesora, który mógłby wykonywać inne czynności. Oczywiście, taki rodzaj kodu jest łatwy do napisania, poprawienia i analizy. Z góry wiemy, jaki stan mają zmienne i jaka część kodu modyfikuje wartość zmiennych. Opóźnienie, które wynika z długotrwałych operacji, umieszczamy linijka po linijce. Zdarzenia występują linearnie, co stanowi dla nas duże ułatwienie.

Nowoczesne aplikacje podczas działania mogą żądać danych z odległego serwera co kilka sekund. W tym czasie generowany lub zmieniany jest interfejs użytkownika. Takie zdarzenia, jak np. kliknięcie przez użytkownika przycisku „Lubię to” albo otrzymanie prywatnej wiadomości, nie mogą zatrzymać ani zamrozić aplikacji. Nie ma tu miejsca na blokowanie wątku ani kodu oraz oczekiwania, aż zakończy się działanie jednej funkcji. Wszystko musi działać płynnie. Dlatego w takich sytuacjach stosuje się programowanie asynchroniczne. Kod nie jest wykonywany linijka po linijce, ale może „skakać” z jednego miejsca na inne. To przeskakiwanie jest odzwierciedleniem oczekiwania na wynik działania poprzedniej operacji (opóźnienie, latency). W programowaniu asynchronicznym musimy brać pod uwagę głównie to opóźnienie – przeskok w czasie podczas pisania kodu. Oczywiście, wielkość tego opóźnienia także nie jest znana z góry ani stała, nawet na tym samym urządzeniu. Warunki działania aplikacji (jej środowisko) są bardzo dynamiczne i losowe.

Problemy z istniejącym kodem

W języku JavaScript, gdzie dla programisty dostępny jest tylko jeden wątek współdzielony przez interfejs i kod aplikacji, mamy dostęp do kilku technik pisania kodu asynchronicznego (nieblokującego). Pierwszym przykładem takiej techniki jest zastosowanie funkcji zwrotnych (callback function). Przekazywane są one zazwyczaj jako argument do innej funkcji, której wykonanie blokuje wątek. Podczas wykonywania długotrwałej operacji działanie programu przechodzi do następnej linijki, jak w programowaniu synchronicznym, ale zaraz po jej zakończeniu kod wraca do przekazanej funkcji zwrotnej, która zostaje wykonana.

pobierzDane('adresSerwera', (wynik) => {
wyswietlDane(wynik);
})
odswierzWiadomosci();

Przedstawiony asynchroniczny kod ilustruje ogólną zasadę wykorzystania funkcji zwrotnych. Najpierw pobieramy dane, przekazując funkcji adres źródła oraz drugą funkcję, w której opisujemy, co zrobić, gdy dane zostaną pobrane. Poza tą funkcją (pobierzDane) mamy kolejną, której zadaniem jest pobranie innych danych. Pierwsza funkcja zostanie uruchomiona i w czasie jej działania zostanie wykonana kolejna – jak w kodzie synchronicznym. Ale po pobraniu danych zostanie wywołana funkcja zwrotna, która przedstawi wyniki. Nie mamy pewności, kiedy wykona się funkcja zwrotna, ponieważ musimy liczyć się tu z omawianym opóźnieniem. Jeżeli pojawiłaby się konieczność wywołania kolejnej funkcji zwrotnej, to w tej już istniejącej będziemy musieli zmagać się z tzw. callback hell, czyli piętrami (schodami) w kodzie. Funkcje zwrotne utrudniają także przechwytywanie błędów, ponieważ bloki try/catch nie wyłapią błędów wyrzuconych przez taką funkcję, gdy zostały zastosowane na funkcji zewnętrznej.

Powyższe problemy częściowo zostały rozwiązane przez nieco ulepszoną technikę. Obietnice (Promise) zostały wprowadzone do języka JavaScript wraz ze specyfikacją ES6. Pomagają one uporać się z chaosem, jaki powstaje w kodzie po wprowadzeniu kilku funkcji zwrotnych. Dodatkowo kod bardziej przypomina wersję synchroniczną:

let pobierzDane = new Promise((sukces, blad)=> {
dlugaOperacja((wynik)=>{
(wynik > 120 ? sukces('sukces) : blad('niepowodzenie);
})
});
pobierzDane
.then((wynik)=>console.log(wynik))
.catch((blad)=>console.log(`Blad: ${blad}`));

Przy tworzeniu obietnicy należy przekazać do konstruktora dwie funkcje, które będą wywołane w zależności od powodzenia lub błędu wykonywanej akcji. W naszym przypadku wywołujemy funkcję dlugaOperacja. Jej wyniki przekazywane będą do utworzonej logiki, w której decydujemy za pomocą warunku, czy wywołanie zakończyło się sukcesem, czy też błędem. Sukces oznacza wywołanie metody w then, a błąd wywołuje catch. Widać, że kod jest trochę bardziej czytelny, a dzięki wywołaniu łańcuchowemu (then, catch) możemy połączyć kilka wywołań funkcji bez konieczności zagnieżdżania ich tak, jak w funkcjach zwrotnych. Udogodnieniem jest jeszcze wprowadzenie kilku operatorów typu Promise, które ułatwiają pracę z większą liczbą funkcji.

Podobnie jak wcześniejsze rozwiązanie, obietnice mają swoje ograniczenie. Każde ich wywołanie wiąże się z utworzeniem nowego obiektu, a to może być uciążliwe dla takich zdarzeń, jak ruch myszą lub reagowanie na naciśnięty klawisz na klawiaturze (powodem jest ich duża częstotliwość). Promise zawsze zwraca tylko jedną wartość, więc ciężko byłoby odbierać asynchroniczne odpowiedzi z serwera tak, jak wiadomości. Jedno połączenie zwróci do klienta tylko tyle danych, ile zwróci serwer, a następnie obiekt Promise przestaje już istnieć. Obietnic także nie możemy anulować po ich rozpoczęciu ani powtórzyć bez konieczności wywołania tego samego kodu. Przykładem jest wysłanie żądania HTTP, które chcielibyśmy anulować po pewnym czasie lub je powtórzyć w przypadku błędu.

Funkcje zwrotne i obietnice mają swoje ograniczenia, ale idealnie nadają się do rzeczy, dla których zostały stworzone – funkcje zwrotne do obsługi zdarzeń, a obietnice do jednorazowych żądań sieciowych. Jeżeli potrzebujemy rozwiązania, które będzie miało zalety wspomnianych technik i pomoże nam w obsłudze nieskończonych zdarzeń i danych, nie spowalniając pracy aplikacji i dając nam czysty i łatwy do zarządzania kod, możemy skorzystać ze strumieni i programowania reaktywnego.

[...]

Autor jest absolwentem Uniwersytetu Ekonomicznego we Wrocławiu, pracuje jako informatyk w Urzędzie Gminy w Miękini.

Pełna treść artykułu jest dostępna w papierowym wydaniu pisma. Zapraszamy do składania zamówień na prenumeratę i numery archiwalne.
 
 

Polecamy

Biblioteka Informacja Publiczna

Specjalistyczne publikacje książkowe dla pracowników administracji publicznej

więcej