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

    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!

     

    Etiquetas:

    Deja una respuesta

    Tu direcci贸n de correo electr贸nico no ser谩 publicada. Los campos obligatorios est谩n marcados con *