Il mondo Js sta pesantemente adottando gli Observable come strumento per la gestione degli eventi asincroni di qualsiasi tipo: dalle richieste ad una API agli eventi del DOM. Questo post non è l’ennesimo getting started sugli Observable: là fuori ce ne sono a migliaia fatti da sviluppatori sicuramente più competenti di me in materia. Oggi vi voglio parlare della nostra esperienza diretta con gli Observable e darvi uno spunto per provare ad utilizzarli nel vostro prossimo progetto. Detto ciò, partiamo!
Le basi: di cosa stiamo parlando?
Gli Observable non sono altro che degli stream di dati. La sorgente di questi dati può essere qualsiasi cosa: una API remota, un evento del DOM oppure della semplice logica applicativa, come ad esempio il classico “event bus”. Per ricevere i dati emessi da un Observable dobbiamo eseguire il “subscribe”. Ogni volta che l’Observable emette un valore verrà invocata una funzione. Fin qui voi mi direte: non ci vedo nulla di eccezionale! Quello che secondo me è la marcia in più degli Observable rispetto alle Promise o alle classiche callback è la possibilità di elaborare il valore emesso prima che questo raggiunga i subscriber, tramite il metodo “pipe”. Ci sono un’infinità di operazioni che possiamo fare sui valori emessi:
- Filtrare alcuni valori affinché non vengano emessi
- Ri-mappare i valori emessi (ad esempio al posto di emettere un oggetto intero possiamo emettere un singolo attributo dell’oggetto)
- Emettere il risultato di un’altro Observable che viene invocato utilizzando il valore emesso dal Observable principale
- Eseguire più Observable in parallelo ed emettere i valori solo quando tutti hanno emesso almeno un valore
- Effettuare il debounce dei valori emessi per evitare delle emissioni troppo ravvicinate (molto utile quando la sorgente è ad esempio un evento del DOM)
- Catturare e gestire eventuali errori
- Gestire logiche di retry anche complesse.
Dimenticavo inoltre di dirvi che in questo post ci stiamo concentrando sul mondo Js ma esistono implementazioni degli Observable praticamente per ogni linguaggio di programmazione moderno. Un motivo in più per prendere familiarità con questo approccio.
Casi pratici d’uso
Abbiamo avuto il nostro primo incontro con gli Observable grazie ad Angular che ne fa un uso abbastanza diffuso in tutto il framework.
Inizialmente non è stato semplice capire a fondo il funzionamento degli Observable, ma una volta presa confidenza li abbiamo sfruttati in ogni situazione. Di seguito vi propongo alcuni esempi pratici di utilizzo, sia per farvi vedere la versatilità dello strumento sia per iniziare a farvi prendere confidenza con i metodi.
La libreria che utilizziamo in questi esempi è RxJS, alla versione 6. Come dicevo prima troverete l’equivalente praticamente per ogni linguaggio moderno: per Java è disponibile RxJava e così via.
Esempio 1: concatenazione di eventi asincroni
Con l’operatore switchMap possiamo prendere il valore emesso da un Observable e usarlo per invocare un altro Observable. I subscribers riceveranno i valori emessi dal secondo Observable. Lo switchMap è molto comodo perché ci permette di evitare annidamenti nel codice rendendolo molto più conciso e leggibile.
this.route.paramMap .pipe(map(params => params.get('id'))) .pipe(switchMap(id => this.api.getProject(id))); .subscribe(project => {...});
Esempio 2: event bus
Utilizzando un Subject possiamo creare un Observable che emette valori in broadcast. Usando un behaviour Subject possiamo addirittura fare in modo che i subscribers ricevano istantaneamente non appena fanno la subscription l’ultimo valore emesso dall’ Observable.
Pensiamo ad esempio di creare un Observable che emette true/false quando l’utente effettua il login/logout. Usando un behaviour Subject siamo sicuri che i subscribers ricevano lo stato corretto dell’utente come primo valore, a prescindere dal momento in cui effettuano la subscription.
// Inizializzazione del Subject con valore di default let authSubject = new BehaviorSubject<boolean>(false); let auth$ = authSubject.asObservable(); // ... // Trigger del login dell'utente authSubject.next(true); // ... // Subscription auth$.subscribe(isLoggedIn => {...});
Esempio 3: campo di input collegato ad API
Penso che ogni sviluppatore web prima o poi si trovi davanti a questa richiesta: realizzare un campo di testo in cui l’utente possa scrivere liberamente e inviare il valore inserito ad una API per effettuare una ricerca. Scrivere MALE un componente che risolva questo problema è molto semplice; scriverlo BENE è un po’ meno semplice, ma con gli Observable possiamo veramente realizzare qualcosa di davvero avanzato. Nello snippet qua sotto abbiamo
- Letto l’evento dal DOM
- Ri-mappato l’evento emesso con il valore corrente nel campo di testo
- Filtrato i valori con meno di 3 caratteri
- Inserito un debounce tra gli eventi in modo che non vengano lanciati troppi eventi ravvicinati
- Filtrato gli eventi il cui valore è uguale al valore precedente
- Concatenato la chiamata all’API
fromEvent(this.searchBox.nativeElement, 'input') .pipe(map(e => e.target.value)) .pipe(filter(value => value.length > 2)) .pipe(debounceTime(500)) .pipe(distinctUntilChanged()) .pipe(switchMap(value => this.api.getSearch(value))); .subscribe(result => {...});
Impressionante, vero?
Esempio 4: logiche di retry
In questo ultimo esempio vediamo come tramite gli Observable possiamo gestire un numero massimo di retry (ad esempio 5) con un delay progressivo tra un retry e l’altro. Una volta raggiunto il numero massimo di retry, l’errore viene inviato ai subscribers.
let maxRetry = 5; this.http.get('https://jsonplaceholderzzzzzzzz.typicode.com/posts/1') .pipe(retryWhen(errors => { let retries = 0; return errors .pipe(tap(error => retries++)) .pipe(switchMap(error => (retries < maxRetry) ? timer(retries * 2000) : throwError(error))); })) .subscribe(value => {...}, err => {...});
Conclusioni
Ci tengo a sottolineare una cosa: gli Observable non ci permettono di risolvere problemi altrimenti impossibili. Tutto quello che risolviamo usando gli Observable può essere risolto in tanti altri modi. La differenza è che, in un mondo in cui dobbiamo avere sempre più a che fare con degli eventi asincroni, avere uno strumento come gli Observable per approcciare il problema ci aiuta sicuramente a scrivere del codice più chiaro, più semplice da manutenere, meno propenso agli errori e, in definitiva, migliore.