Nelle ultime settimane mi sono letto Reactive Programming with RxJava. Oltre che ad approfondire alcuni aspetti degli Observable legati al mondo Java (uno su tutti: I THREAD), ho portato a casa alcuni spunti di utilizzo validi in qualsiasi linguaggio e contesto.

Perché il bello del nostro lavoro è questo, no? Ti leggi un libro relativo a Java, ed il giorno dopo ti ritrovi ad applicare i concetti che hai letto….in un progetto in Angular! 

Andiamo per step:

Gli Observable come interfaccia “comune” di comunicazione asincrona

Abbiamo già parlato degli Observable un po’ di tempo fa e, da allora, abbiamo continuato ad utilizzarli in lungo ed in largo. Il motivo è semplice: con gli Observable riusciamo a modellare qualsiasi fonte asincrona di informazioni, sia essa una chiamata ad una API od una query ad un database locale. Una volta che la nostra “fonte” è modellata da un Observable, possiamo modificarla ed elaborarla in 1000 modi diversi, grazie ai vari “operators” che possiamo applicare prima di emettere effettivamente i valori al subscriber.

Gli Observable come layer di astrazione

Uno dei fattori più importanti in questo approccio è che il subscriber a questo punto non sa, effettivamente, chi genera i valori che sta ricevendo. Arrivano da una chiamata asincrona? Sono eventi del DOM? Non si sà!

Questo ci permette implicitamente di avere un layer di astrazione netto tra chi genera i valori e chi li consuma.

Un esempio concreto: creiamo una cache con soli DUE operators

Proprio sfruttando il layer di astrazione implicito che deriva dall’utilizzo degli Observable è molto semplice creare ad esempio una cache, senza che il subscriber si accorga di nulla. 

Eccovi un esempio concreto: recentemente abbiamo lavorato su una app ibrida realizzata con Ionic (e quindi Angular). Ad ogni avvio dell’app era necessario effettuare delle chiamate ad alcune API, per scaricare informazioni necessarie per l’utilizzo vero e proprio dell’app nelle schermate successive. Come primo approccio abbiamo effettivamente implementato le chiamate “dirette” alle API ad ogni avvio dell’app: funzionava, ma il tempo di avvio dell’app ovviamente ne risentiva, senza considerare che un utente in assenza di connessione non avrebbe potuto in alcun modo accedere all’app.

Le API non erano esposte da noi: non avevamo quindi nemmeno la possibilità di inserire degli header affinché la WebView facesse in autonomia del caching, senza contare che in linea di massima è un approccio poco affidabile e poco controllabile lato WebView.

I dati in questione erano abbastanza statici: si potevano tranquillamente scaricare solo una volta alla settimana senza creare problemi di utilizzo dell’app; da qui è nata l’idea di inserire in qualche modo una cache, che fosse in grado di scaricare i dati, salvarli in locale, ed aggiornarli di tanto in tanto.

Voi mi direte: beh, ma è necessario usare gli Observable per salvarsi due informazioni in uno storage locale? Ovviamente no, ma grazie all’approccio che abbiamo seguito, abbiamo realizzato uno strato di caching potenzialmente applicabile a qualsiasi tipo di Observable che emette un valore singolo, il tutto usando in maniera “tattica” alcuni operators che, visti da fuori, non hanno molti casi d’uso concreto.

Gli operatori concat e first

Eccovi un identikit degli operators che useremo tra poco:

L’operatore concat permette di concatenare due observable omogenei (ovvero che emettono lo stesso tipo di dato) in modo che quando il primo si completa, viene effettuata la subscription al secondo. Chi effettua la subscription da fuori vedrà un unico flusso di valori, che in realtà è generato da due observable distinti.

http://reactivex.io/documentation/operators/concat.html

L’operatore first invece serve per far completare un observable subito dopo il primo valore emesso.

http://reactivex.io/documentation/operators/first.html

La strategia

Vediamo ora come sfruttando i due operators visti sopra possiamo creare una cache.

L’idea di base è la seguente:

  • Abbiamo due Observable: il primo emette il valore dalla cache locale (da qui lo chiameremo ObsCache), il secondo effettua effettivamente la chiamata alle API (da qui lo chiameremo ObsFetch).
  • Tramite l’operatore concat, concateniamo i due observable, mettendo per primo ObsCache. In questo modo quando ObsCache si completa, viene effettuata una subscription a ObsFetch, che emetterà il proprio valore a ruota.
  • Prima di emettere i valori di ObsFetch, andiamo ad aggiornare la cache locale tramite l’operatore tap: http://reactivex.io/documentation/operators/do.html 
  • Tutto ciò non avrebbe molto senso se non dovessimo concludere il flusso con l’operatore first: in questo modo se ObsCache emette un valore, l’Observable complessivo si completa, di fatto non effettuando la subscription a ObsFetch. Nel caso in cui invece ObsCache si concluda senza emettere alcun valore (perché la cache è vuota oppure i valori sono troppo vecchi), viene effettuata la subscription a ObsFetch, andando quindi ad aggiornare la cache ed emettendo poi il valore al subscriber.

Tutto ciò funziona perchè di base gli observable sono cold: non viene eseguito nulla fino a quando qualcuno non effettua una subscription. Questo fa sì che ObsFetch non effettui la chiamata alle API se ObsCache emette un valore grazie all’operatore first, che completa l’Observable complessivo prima che effettivamente venga effettuata la subscription a ObsFetch.

Detto a parole è molto complicato, è molto più semplice scriverne il codice!

L’esempio è basato su Angular 5 e RxJs 5.5, ma con pochi adattamenti può essere trasportato in qualsiasi versione di RxJs e su qualsiasi linguaggio.

Nel CacheProvider ci sono 3 metodi: 

  • putCacheEntry serve per mettere in cache un valore, assegnandogli una determinata chiave (che ci servirà per poterlo ripescare dopo) ed una validità in ore.
  • getCacheEntry ritorna un Observable che emette un valore dalla cache data una chiave. Si occupa anche di filtrare eventuali entry presenti nella cache non più valide. (Notate come sia semplice pescare il valore, filtrarlo e trasformarlo grazie agli operators).
  • getCacheObservable è l’unico metodo pubblico, ed è dove accade la magia. Viene passato in input la chiave che identifica la entry nella cache, l’Observable che vogliamo far passare dal nostro layer di caching e la durata della cache in ore.

A questo punto possiamo prendere qualsiasi nostro Observable che emette un valore singolo e applicargli il nostro layer di caching modificando una sola riga:

prima:

return myObservable;

dopo:

return this.cache.getCacheObservable(“cache_key”, myObservable, 24 * 7);

Tutto qui!

Concludendo

Se avete letto fin qui vi faccio i miei complimenti, vi devono proprio piacere molto gli Observable! A parte gli scherzi, nel mondo dello sviluppo (web) di oggi ormai abbiamo a che fare con eventi asincroni praticamente sempre, e si sa, con gli eventi asincroni è un attimo finire rapidamente in un callback hell

Gli Observable in tutto ciò ci possono davvero aiutare a scrivere codice pulito e semplice, nonostante l’asincronicità. Se poi approfondiamo l’argomento come abbiamo fatto oggi insieme, si vede come una volta che riusciamo a ricondurre tutto ad un Observable, le possibilità diventano infinite ed è poi molto semplice, ad esempio, applicare qualche piccolo operator e creare una cache in poche righe!