Come fare code refactoring di un callback hell con async/await

Ok, hai riaperto una vecchia pagina Javascript (o addirittura monolitica) e ti si incrociano gli occhi. È più forte di te, scorri il codice e trovi una callback hell. Le tue letture sul clean coding e, soprattutto, il tuo forte senso del dovere da buon sviluppatore ti impongono di non fargliela passare liscia.

Che fare? Code refactoring. Inevitabile.

Prendiamo come esempio una funzione grazie alla quale è possibile effettuare il play audio di una frase. La situazione di partenza è questa:

function play_audio(sentence){
   if (sentence.match(/^([A-Za-z]*) ([A-Za-z]*)$/)) {
      var first_file=RegExp.$1;
      var second_file=RegExp.$2;

      var audio_play_first = new Audio(first_file+'.ogg');
         audio_play_first.onended = function() {
            var audio_play_second = new Audio(second_file+'.ogg');
            audio_play_second.play();
         }
         audio_play_first.play();
      }
   }
}

Con questo codice riusciamo ad effettuare il play audio di una semplicissima frase di sole due parole separate da uno spazio, senza particolari segni di interpunzione. La frase viene analizzata dalla regular expression che cattura le due parole separate da uno spazio. A quel punto, avendo a disposizione i file audio associati alle parole, possiamo eseguirli utilizzando l’oggetto Javascript Audio.

Pensiamo ad una frase più complessa, magari composta da quattro-cinque parole. Per effettuare il play audio é necessario innestare una callback ad ogni attributo onended dell’oggetto Audio precedente.

Per gestire una frase relativamente semplice ci sarebbero talmente tante callback innestate che il codice risulterebbe illeggibile, anche su Medium. Evito di presentare un esempio, sarebbe davvero troppo.

Inoltre, questo codice non si comporta dinamicamente: é in grado di gestire solo frasi di lunghezza predeterminata. Se volessi, ad esempio, effettuare una esecuzione di questa funzione passando come argomento una frase da tre parole e successivamente fare un’altra chiamata con una frase da cinque parole, semplicemente non potrei.

Abbiamo due obiettivi dichiarati: pulire e migliorare.

Callback hell

Pulire

In gergo, con “pulire” si intende organizzare meglio il codice e renderlo più leggibile, non variandone minimamente il comportamento. Tecnicamente, un code refactoring.

Come si risolve e si “pulisce” un callback hell? Utilizzando uno strumento Javascript creato appositamente: le Promises.

Le Promises sono dei costrutti che consentono l’esecuzione asincrona di istruzioni che eventualmente restituiranno dei valori. Senza andare nel dettaglio, sappiamo che una Promise va creata e va successivamente consumata con then e, nel momento in cui questo avviene, ne conosciamo il risultato (resolve o reject).

Le Promises consentono una scrittura di codice più organica e chiara perché sintatticamente e logicamente ricordano i linguaggi procedurali. Inoltre, sono concatenabili l’una con l’altra, andando a formare una Promise-chain che è esattamente quello che serve al nostro caso.

const play_file = file => {
   return new Promise(resolve => {
      const audio = new Audio(file+'.ogg');
      audio.onended = function() {
         resolve('audio played')
      }
      audio.play();
   })
}

play_file(first_file)
.then(res => {
   return play_file(second_file)
})
.then(res => {
   return play_file(third_file)
})

Migliorare

Decisamente più leggibile, soprattutto su frasi di diverse parole. Utilizzando le Promises si comprende a vista d’occhio quello che viene eseguito dal browser. Tuttavia, la soddisfazione non è completa. Le Promises hanno un difetto: è impossibile spezzarne la catena di then. Il che, nel nostro caso, significa solo una cosa: impossibile rendere dinamico il flusso audio.

In questo caso ci vengono in aiuto le funzioni async/await, che si basano sulle stesse primitive delle Promises ma sono, per l’appunto, funzioni: non esistono catene e, soprattutto, possiamo effettuare un ciclo sulla chiamata a funzione play_audio, passandogli come parametro un elemento di una lista composta da tutte le parole di una frase. Abbiamo reso il nostro codice dinamico e possiamo effettuare il play di frasi di diversa lunghezza, senza seguire un formalismo predefinito.

Questa la realizzazione finale:

const play_audio = file => {
   return new Promise(resolve => {
      const audio = new Audio(file+'.ogg');
      audio.onended = function() {
         resolve('audio played')
      }
      audio.play();
   })
}

async function play_audio_list(files) {
   for (const file of files) {
      await play_audio(file);
   }
}

let audio_words = []
audio_words = sentence.split(' ')

play_audio_list(audio_words)

Pulito, migliorato, leggibile. Un gran bel respiro di freschezza.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.