Podejrzewam, że o programowaniu reaktywnym słyszał każdy programista. Ja temat zgłębiam od niedawna i postanowiłem zebrać kilka najważniejszych informacji, które pomogą Wam szybko wystartować. Będę mieszał krótkie opisy teorii z praktycznymi przykładami.

Istnieje duże prawdopodobieństwo, że nauka programowania reaktywnego zmieni całkowicie Wasze podejście do pisania kodu. Będziecie potrzebowali trochę czasu żeby się przestawić, ale naprawdę warto. Musicie jednak pamiętać, że podejście reaktywne nie rozwiązuje wszystkich problemów i nie w każdym projekcie się sprawdzi. W małych projektach na przykład może okazać się niepotrzebną komplikacją. Najlepiej traktować je jak jedno z dostępnych narzędzi, po które będziecie sięgać w miarę potrzeby.

Na co dzień korzystam głównie z języka Swift, dlatego w tym artykule będę wykorzystywał bibliotekę RxSwift. Programowanie reaktywne może być zaimplementowane jednak w większości popularnych języków programowania, dlatego przedstawione tutaj koncepcje można traktować jako uniwersalne. Oczywiście w poszczególnych językach będą występowały różnice w samej implementacji. W artykule będę czasami posługiwał się nazewnictwem oryginalnym. Tak będzie po prostu łatwiej, bo szukając informacji na dany temat i tak będziecie musieli korzystać ze stron anglojęzycznych.

 

 

Co się za tym kryje

 

 

Programowanie reaktywne jest asynchronicznym paradygmatem opartym na sekwencjach danych oraz obserwatorach. 

Największą bolączką w sposobie programowania, który większość z nas stosuje na co dzień, są tzw. „efekty uboczne”, z którymi mamy do czynienia, gdy napisana przez nas funkcja musi uzyskać dostęp do elementu znajdującego się poza jej kontekstem. Najlepszym przykładem są tutaj pola klas, które służą do przechowywania stanu naszego obiektu (np. wysokość pensji w klasie Person). Jeżeli takie pole nie zostanie zdefiniowane jako stała, to istnieje duże ryzyko, że pewnym momencie stracimy możliwość śledzenia zmian zachodzących w tym polu. To z kolei może doprowadzić do powstania bugów, które będzie bardzo trudno namierzyć. Wyobraźcie sobie sytuację, w której wartość pewnej zmiennej jest edytowana w wielu miejscach, a Wy podczas debugowania musicie weryfikować stan po każdej operacji. Nic fajnego 😣. Dodatkowym utrudnieniem jest wielowątkowość obecna dzisiaj w każdej aplikacji. Bez odpowiedniego zabezpieczenia nie będziemy mieli pewności, czy na danym polu swoich operacji nie wykonują dwa wątki jednocześnie.

Programowanie reaktywne próbuje temu zaradzić korzystając z zalet programowania funkcyjnego. U jego podstaw leżą funkcje (no shit Sherlock 😂), które mogą przyjmować inne funkcje jako argumenty oraz zwracać je jako wynik obliczeń, podobnie jak zwykłe zmienne. Nazywane są funkcjami wyższego rzędu lub w języku angielskim higher-order functions.

Programowanie reaktywne jest także w pewnym sensie rozbudowaną wersją wzorca projektowego o nazwie Observer Pattern, w którym określone obiekty przyglądają się zmianom zachodzącym w innych obiektach. Różnica polega na tym, że w „reaktywnym kodzie” na podstawie zachodzących zmian podejmowane są od razu określone działania.

Po tym krótkim wstępie możemy przejść do omawiania poszczególnych elementów 😏.

 

 

Observable Sequences

 

 

Podstawowy element programowania reaktywnego. Wszystko z czym będziemy mieli do czynienia podczas wykorzystania biblioteki RxSwift (lub podobnej) będziemy traktowali jako sekwencję lub element, który z nią współpracuje. Tak wyglądają przykładowe implementacje:

 

 

Observables (obiekty, które będą obserwowane) działają asynchroniczne, co oznacza, że nie jesteśmy w stanie przewiedzieć kiedy poszczególne operacje zostaną wykonane. Kiedy jednak tak się stanie, Observable wyemituje odpowiednią informację / zdarzenie w języku angielskim określane jako Event. Jej odbiorcą będzie każdy obiekt, który zapisał się na listę zainteresowanych. Aby jednak Observable emitował jakiekolwiek Eventy, musi istnieć przynajmniej jeden obiekt, który będzie chciał ich nasłuchiwać.

Dodamy teraz trzech obserwatorów dla sekwencji przedstawionych w powyższym przykładzie:

 

 

 

 

Każdy Observable w trakcie swojego cyklu życia jest w stanie wyemitować zero lub więcej zdarzeń. W RxSwift Eventy są zwykłą enumeracją, która składa się z trzech przypadków:

.next(Element) – w momencie dodania pojedynczej wartości lub całej kolekcji do Observable, wyemituje on zdarzenie Next Event, które zostanie odebrane przez wszystkich Subscribers, czyli obiekty traktowane jako odbiorcy.

.error(Swift.Error)Event zostanie wyemitowany w przypadku napotkania błędu. W tym samym momencie Observable zakończy nadawanie.

.completedEvent zostanie wyemitowany w przypadku poprawnego zakończenia działania Observable.

Tak wygląda pełna implementacja enumeracji, którą skopiowałem z oficjalnej dokumentacji:

 

 

Rozbudujemy trochę pierwszy z obserwatorów, aby zobaczyć dokładnie jak emitowane są poszczególne Eventy:

 

 

Możemy bez problemu zapisać się do nasłuchiwania tylko jednego rodzaju Eventu, na przykład .next. Zamiast samego zdarzenia, jesteśmy w stanie pobrać wartość, która została w nim przekazana:

 

 

W dowolnym momencie możemy również wypisać dany obiekt z listy obserwujących za pomocą funkcji dispose().

Dla ułatwienia możemy również skorzystać z obiektu DisposeBag, który będzie dla nas automatycznie zarządzał subskrypcjami. Tuż przed usunięciem DisposeBag z pamięci, dla każdej z subskrypcji z osobna zostanie wywołana metoda dispose(). Korzystając ponownie z pierwszego obserwatora:

 

 

A po co właściwie to wszystko? Jeżeli nie pozbędziemy się subskrypcji po jej wykorzystaniu, to ryzykujemy, że powstanie tzw. wyciek pamięci. Więcej na ten temat możecie przeczytać w tym miejscu.

Jeżeli nie chcemy tworzyć Observables, które tylko czekają, aż ktoś się do nich zapisze, to możemy skorzystać z Observable Factories, które będą dostarczały nowy obiekt Observable dla każdego subskrybenta.

 

 

Marble Diagrams

 

 

Zanim przejdziemy do omawiana bardziej złożonych zagadnień musimy zapoznać się z tak zwanymi Marble Diagrams. Możemy z ich pomocą wizualizować operacje wykonywane na Observable Sequences. Wszystkie diagramy, które będę wykorzystywał w tym wpisie pochodzą ze strony RxMarbles. Poniżej umieściłem przykładowy diagram z operacją Merge, która łączy kilka sekwencji:

 

 

Dwa pierwsze wykresy reprezentują osobne strumienie danych, następnie jest określana operacja (w tym wypadku merge), a na samym końcu prezentowany jest wynik operacji. Nic skomplikowanego 😎.

 

 

Subjects

 

 

Specjalny typ Observable Sequences, który pozwala na dodawanie nowych elementów do zbioru obserwowanych danych. W pewnym sensie Subjects stanowią połączenie nadawcy (Observable) oraz odbiorcy (obiektu nasłuchującego).

Możemy wyróżnić cztery rodzaje Subjects:

– PublishSubject – po zapisaniu się na jego „listę dystrybucyjną” będziemy otrzymywali Eventy, ale tylko te wyemitowane po dokonaniu subskrypacji. Nowy obiekt PublishSubject nie zawiera żadnych danych.

– BehaviorSubject – przekazuje do odbiorców najbardziej aktualny element. Obiekt ten jest tworzony z pewnym zakresem danych początkowych, czyli nie może być pusty.

– ReplaySubject – rozbudowana wersja BehaviorSubject, która pozwala określić jaka ilość danych zostanie ponownie wysłana do nowych odbiorców. Obiekt ten tworzony jest z buforem danych, którego rozmiar określa liczbę elementów do powtórzenia. Warto zanotować sobie, że bufor ten będzie przechowywany w pamięci urządzenia, tak więc musimy postępować z nim bardzo rozważnie.

– Variable – obiekt ten zbudowany jest na bazie BehaviorSubject i jest wykorzystywany do zapisywania jego aktualnej wartości. Działa bardzo podobnie do zwykłej zmiennej.

 

Poniżej przykład z ReplaySubject:

 

 

 

Operacje na danych w sekwencjach

 

 

Jeżeli zajdzie taka potrzeba możemy dokonać selekcji / edycji danych emitowanych przez Observable Sequence, zanim te dotrą do obiektów nasłuchujących.

Do dyspozycji mamy całkiem pokaźną liczbą operatorów. Tutaj zaprezentuję Wam tylko kilka z nich. Po więcej możecie zajrzeć do oficjalnej dokumentacji.

 

Selekcja danych w strumieniach

 

– ignoreElements – z jego pomocą będziemy ignorować Eventy typu .next, czyli wszystkie wartości pochodzące z Observable Sequence. Przepuszczane będą natomiast Eventy typu .completed oraz .error. Operator ten może się przydać, gdy będziemy chcieli otrzymać jedynie informację, że dany Observable zakończył już emitowanie Eventów:

 

 

 

Wywołanie kolejnych Eventów .next nie spowoduje wyświetlenia wiadomości w konsoli:

Dopiero użycie onCompleted() wywoła reakcję:

 

 

– filter – na podstawie określonego przez nas warunku operator ten będzie dokonywał selekcji danych ze strumienia. Działa tak samo jak standardowa funkcja filter z biblioteki Swift. Warunek umieszczany jest w niezależnym bloku kodu:

 

 

 

 

Przekształcanie danych w strumieniach

 

– map – działa tak samo jako funkcja map ze standardowej biblioteki Swfit. Na podstawie operacji określonej w closure dokonuje przekształcenia poszczególnych elementów w strumieniu:

 

 

 

 

– flatMap – dokonuje przekształcenia poszczególnych elementów, a dodatkowo łączy ze sobą poszczególne strumienie. Wykres wyjątkowo pochodzi ze strony reactivex.io:

 

 

 

 

Łączenie danych w strumieniach

 

– startWith – dodaje określoną przez nas wartość na sam początek sekwencji:

 

 

 

 

– concat – łączy ze sobą poszczególne strumienie:

 

 

 

 

Schedulers

 

Operatory dokonujące operacji na danych w strumieniach, działają na tym samym wątku co ich subskrypcje. Jeżeli chcemy, aby wykonywały operacje na innych wątkach to musimy skorzystać ze Schedulers. Tworzą one dla nas kontekst, dzięki któremu możemy określić gdzie dane operacje powinny zostać wykonane.

Należy jednak pamiętać, że Schedulers nie są wątkami, ani ich też nie tworzą. Zamiast tego korzystają one pod maską z takich bibliotek jak Grand Central Dispatch oraz NSOperation. Wynika z tego również, że Schedulers mogą być zarówno współbieżne, jak i seryjne.

Za pomocą operatora subscribeOn mamy możliwość określenia na jakim wątku będzie wykonywany kod emitujący poszczególne strumienie. Z kolei observeOn pozwoli nam wskazać wątek, na który wszystkie Eventy mają zostać dostarczone. Tak to będzie wyglądało na przykładzie:

 

 

A tak wygląda lista dostępnych Schedulers:

– MainScheduler – operuje na wątku głównym, zwanym również wątkiem UI. To na nim wykonywane są wszystkie operacje związane z interfejsem użytkownika oraz inne priorytetowe zadania.

– SerialDispatchQueueScheduler – dzięki niemu możemy wykonywać zadnia w ramach seryjnej kolejki DispatchQueue będącej częścią GDC.

– ConcurrentDispatchQueueScheduler – działa podobnie do SerialDispatchQueueScheduler, jednak zadania są wykonywane współbieżnie.

– OperationQueueScheduler – jego działanie jest zbliżone do ConcurrentDispatchQueueScheduler, z tą różnicą, że pod spodem wykorzystywane są NSOperationQueues, zamiast DispatchQueues.

– TestScheduler – specjalny Scheduler przeznaczony do pisania testów dla kodu reaktywnego. Nie powinien być mieszany z kodem produkcyjnym.

 

Słowo na dorgę

 

Myślę, że takie wprowadzenie ułatwi kilku osobom rozpoczęcie swojej przygody z nowy paradygmatem programowania. Do podejścia reaktywnego trzeba się przyzwyczaić. Artykuł ten to tylko wierzchołek góry lodowej, a po więcej informacji koniecznie będziecie musieli zajrzeć do dokumentacji biblioteki Rx zaimplementowanej w danym języku. Dla mnie programowanie reaktywne to w pewnym sensie kolejny, mały krok w ewolucji. Nie twierdzę, że sprawdzi się w każdym projekcie, ale tam gdzie zostanie użyte z pewnością wniesie dużo dobrego.

 


 

Dodaj komentarz

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