Gestire i dati i maniera asincronaPROMISE e OBSERVABLE

Netfarm

La sincronizzazione nelle App Frontend

Nello sviluppo di applicazioni frontend, qualsiasi sia il framework utilizzato (Angular, Vue.js, Jquery, etc..), ci si scontra ben presto con il concetto di asincronia delle comunicazioni.
E’ infatti comune che all’interno della nostra app vengano fatte delle chiamate a dei servizi remoti per richiedere dei dati, chiamate di cui non si conosce il tempo di risposta: per questo motivo non si possono gestire queste request in maniera sincrona, dato che, questo porterebbe ad un “freeze” dell’interfaccia per tutto il tempo di attesa della response.

Ecco che quindi ci vengono in aiuto Promise e Observable, oggetti diversi ma che riescono entrambi a gestire in maniera efficiente le operazioni asincrone.

PROMISE 


Promise è un oggetto che fa parte della libreria standard di Javascript e rappresenta esplicitamente, nella sua logica, lo stato (successo o fallimento) di un’operazione asincrona ed il suo valore di ritorno.

Per quanto riguarda lo stato di esecuzione di una Promise, quest’ultimo può trovarsi in uno dei seguenti casi:

pending → stato iniziale, in attesa di completamento
fulfilled → operazione completata con successo
rejected → operazione fallita

L’handling dei due stati finali (successo e fallimento) può essere implementato tramite l’utilizzo dei metodi then() e catch(), che possono essere anche concatenati tra loro, dato che restituiscono entrambi Promise

Qui sotto un esempio:


/* Dichiarazione di un Promise */
const myPromise = (new Promise(myExecutorFunc))
.then(handleFulfilledA)
.then(handleFulfilledB)
.then(handleFulfilledC)
.catch(handleRejectedAny);

/* Si possono gestire le callback di try e catch direttamente */
const myPromise = (new Promise(myExecutorFunc))
.then(data => { /* Fai qualcosa con i dati */ })
.catch(error => { /* Fai qualcosa con l’errore */ });



In questo esempio, viene:

1.   Creato un nuovo oggetto di tipo Promise come risultato della funzione passata per argomento al costruttore,
2.  Dopodiché viene manipolato più volte in maniera sequenziale tramite l’utilizzo del metodo then(),
3.  Infine viene gestita l’eventuale situazione di errore tramite il metodo catch().

Proiettando questo esempio in uno scenario di chiamate asincrone verso un server, “myExecutorFunc” rappresenta la nostra funzione che esegue la chiamata e restituisce un Promise, i vari “handleFulfilled” le funzioni che gestiscono i dati di risposta qualora questa venisse completata con successo (ad esempio aggiornare una tabella o un form con i dati della response), mentre “handleRejectedAny” rappresenta la gestione degli errori della chiamata (ad esempio mostrare un alert con un messaggio o riempire un log).

DA RICORDARE

Una cosa importante da ricordare è che la callback fornita al costruttore di un Promise è eseguita immediatamente, per cui scrivere logica all’interno di quest’ultima significa eseguire operazioni prima che venga scatenata la callback del metodo then: per questo motivo si dice che Promise è “eager”.


const myPromise = new Promise((resolve, reject) => {
console.log('Callback costruttore');
try {
dosomething(…);
resolve(…);
} catch (e) {
reject(e);
}
});

console.log('Prima di chiamare la then');
myPromise.then(res => console.log('Gestione dei dati di ritorno'));

Se eseguiamo questo codice, i messaggi compariranno a schermo in quest’ordine:

    
    Callback costruttore
    Prima di chiamare la then
    Gestione dei dati di ritorno


UTILIZZARE IL METODO AWAIT

Un altro modo per gestire gli oggetti Promise, molto più “pulito” a livello di leggibilità, è costituito dall’utilizzare l’operatore await ed assegnare direttamente il suo risultato, ovvero il valore "fulfilled" del Promise, ad una costante: in questo modo si possono gestire i dati di ritorno come in un normale flusso senza dover concatenare metodi.

Nell’utilizzo di questa modalità, però, lo snippet di codice che chiama il metodo await deve essere necessariamente inserito in una funzione asincrona e all’interno di un blocco try – catch, dato che await lancia un’eccezione in caso di errore.
Di seguito un esempio completo di quando appena detto:


async function getUtenti() {
try {
const resp = await fetch('https://url-chiamata.com/');
const data = await resp.json();
gestoreDatiDiRitorno(data);
} catch (error) {
console.log(error);
}
}


Come si vede, il codice risulta più leggibile e compatto, per cui questa soluzione è altamente consigliata.

OBSERVABLE


                                                                                                                                           Source Img https://stackoverflow.com/


Observable è un emettitore di valori multipli. Rappresenta il core type di Reactive Xuna libreria atta alla programmazione asincrona e “event-based”. 



Si basa sul concetto che i dati futuri vengono “osservati”, ovvero tenuti sotto controllo dagli Observer (ovvero oggetti che agiscono come “consumers”), che si registrano su quei dati (diventando appunto “subscribers”) e quindi possono gestirli non appena questi saranno disponibili. Questa registrazione avviene tramite il metodo subscribe()  di Observable, il quale accetta per l’appunto un Observer come argomento.

const observer = {
next: x => console.log('Observer ha ricevuto il prossimo valore: ' + x),
error: err => console.error('Observer ha ricevuto un errore: ' + err),
complete: () => console.log('Observer ha ricevuto una notifica di completamento'),
};


Gli Observer hanno tre callbacks per gestire gli Observable a cui sono registrati:

next → rappresenta il valore attuale emesso dall’Observable
error → rappresenta l’eventuale stato di errore
complete → rappresenta la notifica di completamento dell’operazione che ha generato l’Observable.


NB: queste tre callback vengono chiamate ad ogni valore emesso da Observable.
A differenza di come succede nei Promise, la callback del costruttore di un Observable non viene eseguita immediatamente, ma viene invocata all’invocazione del metodo subscribe: per questo motivo si dice che un Observable è “lazy”.

 

   
const observer = {
next: x => console.log('Observer ha ricevuto il prossimo valore: ' + x),
error: err => console.error('Observer ha ricevuto un errore: ' + err),
complete: () => console.log('Observer ha ricevuto una notifica di completamento'),
};
observable.subscribe(observer);

  Eseguendo il codice qui sopra, l'output sarà il seguente:

    
    Prima di chiamare la subscribe
    Callback costruttore
    Completamento operazione



Quando si invoca la subscribe di un Observable, non è necessario passare come argomento un Observer, dato che quest’ultimo può essere sostituito direttamente dalle sue callbacks, come nel seguente esempio:

 


myObservable.subscribe(x => console.log('Ottenuto nuovo valore' + x),
error => console.error('Riscontrato errore' + error));

Nello scenario, descritto in precedenza, di una chiamata HTTP, si wrappa la risposta della chiamata in un Observable (questo è possibile ad esempio utilizzando l’operatore from() di rxjs→ https://rxjs-dev.firebaseapp.com/api/index/function/from), su quest’ultimo si esegue la subscribe() e nella callback si esegue la logica sul dato ricevuto.

Facendo un esempio pratico, supponendo di avere una funzione che esegue una richiesta HTTP per ottenere una lista di utenti, si procede nel seguente modo


   

import { from } from 'rxjs';

const users = getUsersFromHttpRequest();
const observableUsers = from(users);
observableUsers.subscribe(users => {
/* Qui va la logica di gestione del dato, ovvero users */
});

Rxjs fornisce anche una serie di operatori applicabili agli Observable, anche in maniera sequenziale, che consentono di creare una vera e propria catena di manipolazione dei dati. Per un riferimento completo agli operatori, fare riferimento a questa pagina https://rxjs-dev.firebaseapp.com/guide/operators.


PROMISE vs OBSERVABLE
Quale soluzione scegliere?

Innanzitutto, tenere presente le differenze principali fra i due approcci, che sono:

1. Promise è “eager”, Observable è “lazy”, ovvero la callback del costruttore di Promise viene eseguita immediatamente mentre quella di Observable invece solo invocando la subscribe

2. Promise emette un singolo valore, Observable può emettere valori multipli

3. Promise non è cancellabile, Observable si (può anche essere riprovata l’emissione in caso di errore)

4. Observable ha accesso ad una serie di operatori utili per la manipolazione dei dati

      
Source Img https://stackoverflow.com/


Appare quindi chiaro che gli Observable sono più completi dei Promise, ma non per questo sono preferibili in tutte le occasioni.

Quando utilizzare i Promise

Ad esempio, quando si ha una singola operazione asincrona da eseguire e si vuole semplicemente processarne il risultato, è preferibile usare Promise, dato che le sue potenzialità sono più che sufficienti allo scopo, inoltre la sua implementazione tramite l’uso di async e await semplifica di molto l’implementazione.

Quando utilizzare gli Observable

1.  Gestire uno stream di dati continui nel tempo
Quando invece si vuole gestire uno stream di dati che vengono forniti continuativamente nel tempo, va utilizzato Observable, che tramite le sue callback chiamate ad ogni valore emesso, permettono un controllo del flusso di dati e degli eventuali errori, più la possibilità di fare rollback delle operazioni  e di ripeterle.

2.  Modificare i dati emessi in maniera profonda
Infine, Observable è consigliabile anche quando si devono rimaneggiare in maniera profonda i dati emessi, dato che gli operatori di Rxjs sono molto potenti e restituiscono a loro volta degli Observable, per cui sono concatenabili in maniera sequenziale.

E’ infine utile menzionare che è possibile convertire un Promise in un Observable e viceversa, rispettivamente tramite i due metodi from() e toPromise() di rxjs:


import { from, toPromise } from 'rxjs';

const observableFromPromise = from(promiseObject);
const promiseFromObservable = observableObject.toPromise()