Evitar el infierno de devolución de llamada en Node.js

E

Introducción

Admito que fui una de esas personas que decidió aprender Node.js simplemente por el rumor que lo rodeaba y lo mucho que todos hablaban de él. Pensé que debe haber algo especial en él si tiene tanto apoyo tan temprano en su vida. La mayoría de las veces provenía de C, Java y Python, por lo que el estilo asincrónico de JavaScript era muy diferente a todo lo que había encontrado antes.

Como muchos de ustedes probablemente saben, todo lo que JavaScript está realmente debajo es un ciclo de eventos de un solo subproceso que procesa los eventos en cola. Si ejecutara una tarea de larga duración dentro de un solo hilo, el proceso se bloquearía, lo que haría que otros eventos tuvieran que esperar para ser procesados ​​(es decir, la interfaz de usuario se bloquea, los datos no se guardan, etc.). Esto es exactamente lo que desea evitar en un sistema impulsado por eventos. Aquí es un gran video que explica mucho más sobre el ciclo de eventos de JavaScript.

Para resolver este problema de bloqueo, JavaScript se basa en gran medida en las devoluciones de llamada, que son funciones que se ejecutan después de que ha finalizado un proceso de ejecución prolongada (IO, temporizador, etc.), lo que permite que la ejecución del código continúe más allá de la tarea de ejecución prolongada.

downloadFile('example.com/weather.json', function(err, data) {
	console.log('Got weather data:', data);
});

El problema: el infierno de devolución de llamada

Si bien el concepto de devoluciones de llamada es excelente en teoría, puede generar un código realmente confuso y difícil de leer. Imagínese si necesita hacer una devolución de llamada después de la devolución de llamada:

getData(function(a){
    getMoreData(a, function(b){
        getMoreData(b, function(c){ 
            getMoreData(c, function(d){ 
	            getMoreData(d, function(e){ 
		            ...
		        });
	        });
        });
    });
});

Como puede ver, esto realmente puede salirse de control. Agregue algunos if declaraciones, for bucles, llamadas a funciones o comentarios y tendrá un código muy difícil de leer. Los principiantes especialmente son víctimas de esto, sin comprender cómo evitar esta “pirámide de la fatalidad”.

Alternativas

Diseño a su alrededor

Muchos programadores quedan atrapados en el infierno de las devoluciones de llamada solo por esto (diseño deficiente). Realmente no piensan en la estructura de su código con anticipación y no se dan cuenta de lo mal que se ha vuelto su código hasta que es demasiado tarde. Al igual que con cualquier código que esté escribiendo, debe detenerse y pensar en lo que se puede hacer para que sea más simple y más legible antes o mientras lo escribe. Aquí hay algunos consejos que puede utilizar para evitar el infierno de devolución de llamada (o al menos gestionarlo).

Usar módulos

En casi todos los lenguajes de programación, una de las mejores formas de reducir la complejidad es modularizar. La programación de JavaScript no es diferente. Siempre que esté escribiendo código, tómese un tiempo para dar un paso atrás y averiguar si ha habido un patrón común que encuentre con frecuencia.

¿Está escribiendo el mismo código varias veces en diferentes lugares? ¿Las diferentes partes de su código siguen un tema común? Si es así, tiene la oportunidad de limpiar las cosas y abstraer y reutilizar el código.

Hay miles de módulos que puede consultar como referencia, pero aquí hay algunos para considerar. Manejan tareas comunes, pero muy específicas, que de otro modo saturarían su código y reducirían la legibilidad: Pluralizar, csv, qs, clon.

Dale nombres a tus funciones

Al leer código (especialmente código desordenado y desorganizado), es fácil perder la pista del flujo lógico, o incluso la sintaxis, cuando los espacios pequeños están congestionados con tantas devoluciones de llamada anidadas. Una forma de ayudar a combatir esto es nombrar sus funciones, por lo que todo lo que tendrá que hacer es mirar el nombre y tendrá una mejor idea de lo que hace. También le da a sus ojos un punto de referencia de sintaxis.

Considere el siguiente código:

var fs = require('fs');

var myFile="/tmp/test";
fs.readFile(myFile, 'utf8', function(err, txt) {
    if (err) return console.log(err);

    txt = txt + 'nAppended something!';
    fs.writeFile(myFile, txt, function(err) {
        if(err) return console.log(err);
        console.log('Appended text!');
    });
});

Ver esto puede llevarle unos segundos para darse cuenta de lo que hace cada devolución de llamada y dónde comienza. Agregar un poco de información adicional (nombres) a las funciones puede marcar una gran diferencia para la legibilidad, especialmente cuando tiene múltiples niveles en devoluciones de llamada:

var fs = require('fs');

var myFile="/tmp/test";
fs.readFile(myFile, 'utf8', function appendText(err, txt) {
    if (err) return console.log(err);

    txt = txt + 'nAppended something!';
    fs.writeFile(myFile, txt, function notifyUser(err) {
        if(err) return console.log(err);
        console.log('Appended text!');
    });
});

Ahora, un vistazo rápido le dirá que la primera función agrega texto mientras que la segunda función notifica al usuario del cambio.

Declare sus funciones de antemano

Una de las mejores formas de reducir el desorden del código es manteniendo una mejor separación del código. Si declara una función de devolución de llamada de antemano y la llama más tarde, evitará las estructuras profundamente anidadas que hacen que la devolución de llamada sea tan difícil de trabajar.

Entonces podrías pasar de esto …

var fs = require('fs');

var myFile="/tmp/test";
fs.readFile(myFile, 'utf8', function(err, txt) {
    if (err) return console.log(err);

    txt = txt + 'nAppended something!';
    fs.writeFile(myFile, txt, function(err) {
        if(err) return console.log(err);
        console.log('Appended text!');
    });
});

…a esto:

var fs = require('fs');

function notifyUser(err) {
    if(err) return console.log(err);
    console.log('Appended text!');
};

function appendText(err, txt) {
    if (err) return console.log(err);

    txt = txt + 'nAppended something!';
    fs.writeFile(myFile, txt, notifyUser);
}

var myFile="/tmp/test";
fs.readFile(myFile, 'utf8', appendText);

Si bien esta puede ser una excelente manera de ayudar a aliviar el problema, no lo resuelve por completo. Al leer el código escrito de esta manera, si no reString exactamente qué hace cada función, tendrá que volver atrás y mirar cada una para volver sobre el flujo lógico, lo que puede llevar tiempo.

Async.js

Afortunadamente, bibliotecas como Async.js existen para tratar de frenar el problema. Async agrega una capa delgada de funciones sobre su código, pero puede reducir en gran medida la complejidad al evitar el anidamiento de devolución de llamada.

Existen muchos métodos de ayuda en Async que se pueden usar en diferentes situaciones, como serie, paralelo, cascada, etc. Cada función tiene un caso de uso específico, así que tómate un tiempo para aprender cuál te ayudará en qué situaciones.

Tan bueno como Async es, como todo, no es perfecto. Es muy fácil dejarse llevar por la combinación de series, paralelos, para siempre, etc., momento en el que vuelves al punto de partida con un código desordenado. Tenga cuidado de no optimizar prematuramente. El hecho de que algunas tareas asincrónicas se puedan ejecutar en paralelo no siempre significa que deban hacerlo. En realidad, dado que Node es de un solo subproceso, ejecutar tareas en paralelo al usar Async tiene poca o ninguna ganancia de rendimiento.

El código de arriba se puede simplificar usando la cascada de Async:

var fs = require('fs');
var async = require('async');

var myFile="/tmp/test";

async.waterfall([
    function(callback) {
        fs.readFile(myFile, 'utf8', callback);
    },
    function(txt, callback) {
        txt = txt + 'nAppended something!';
        fs.writeFile(myFile, txt, callback);
    }
], function (err, result) {
    if(err) return console.log(err);
    console.log('Appended text!');
});

Promesas

Aunque las promesas pueden tomar un poco de comprensión, en mi opinión son uno de los conceptos más importantes que puedes aprender en JavaScript. Durante el desarrollo de uno de mis Aplicaciones SaaS, Terminé reescribiendo todo el código base usando Promises. No solo redujo drásticamente el número de líneas de código, sino que hizo que el flujo lógico del código fuera mucho más fácil de seguir.

A continuación, se muestra un ejemplo que utiliza la biblioteca Promise, muy rápida y muy popular, Azulejo:

var Promise = require('bluebird');
var fs = require('fs');
Promise.promisifyAll(fs);

var myFile="/tmp/test";
fs.readFileAsync(myFile, 'utf8').then(function(txt) {
	txt = txt + 'nAppended something!';
	fs.writeFile(myFile, txt);
}).then(function() {
	console.log('Appended text!');
}).catch(function(err) {
	console.log(err);
});

Observe cómo esta solución no solo es más corta que las soluciones anteriores, sino que también es más fácil de leer (aunque, es cierto, el código de estilo Promise puede tomar algún tiempo para acostumbrarse). Tómese el tiempo para aprender y comprender las Promesas, valdrá la pena su tiempo. Sin embargo, las Promesas definitivamente no son la solución a todos nuestros problemas en la programación asincrónica, así que no asuma que al usarlas tendrá una aplicación rápida, limpia y sin errores. La clave es saber cuándo le serán útiles.

Algunas bibliotecas de Promise que debe consultar son Q, Azulejo, o el promesas incorporadas si está utilizando ES6.

Async / Await

Nota: Esta es una función de ES7, que actualmente no es compatible con Node o io.js. Sin embargo, puedes usarlo ahora mismo con un transpilador como Babel.

Otra opción para limpiar su código, y mi futuro favorito (cuando tiene un soporte más amplio), es usar async funciones. Esto le permitirá escribir código que se parece mucho más a un código síncrono, pero que sigue siendo asincrónico.

Un ejemplo:

async function getUser(id) {
    if (id) {
        return await db.user.byId(id);
    } else {
        throw 'Invalid ID!';
    }
}

try {
	let user = await getUser(123);
} catch(err) {
	console.error(err);
}

los db.user.byId(id) la llamada devuelve un Promise, que normalmente tendríamos que usar con .then(), pero con await podemos devolver el valor resuelto directamente.

Observe que la función que contiene el await la llamada tiene el prefijo async, que nos dice que contiene código asincrónico y también se debe llamar con await.

Otra gran ventaja de este método es que ahora podemos usar try/catch, fory while con nuestras funciones asincrónicas, que es mucho más intuitivo que encadenar promesas.

Aparte de usar transpiladores como Babel y Traceur, también puede obtener una funcionalidad como esta en Node con el asyncawait paquete.

Conclusión

Evite problemas tan comunes como que el infierno de las devoluciones de llamada no es fácil, así que no espere terminar con sus frustraciones de inmediato. Todos quedamos atrapados en eso. Intente reducir la velocidad y tómese un tiempo para pensar en la estructura de su código. Como todo, la práctica hace al maestro.

¿Has corrido al infierno de devolución de llamada? Si es así, ¿cómo se soluciona? ¡Dinos en los comentarios!

 

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias para su correcto funcionamiento. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad