Pequeños trucos para mejorar tu JavaScript

6249570814_3bc6b649ea_b.jpg

Foto de Steve-h

En algunas ocasiones, un pequeño truco puede ayudarnos a mejorar la forma en la que escribimos nuestro código. A veces son cosas que ya tenemos en cuenta, otras por el contrario, no nos aporta nada o no estamos de acuerdo en la manera de plantearlo.

Llevo tiempo queriendo escribir en el blog sobre algo así, pequeños trucos que definan nuestra forma de escribir código. Como no, el lenguaje de donde más trucos me llego a saber es en JavaScript. No son nada del otro mundo, pero si son acciones que suelo tener en cuenta cuando escribo mi código.

Espero que os gusten:

Cómo evitar la estructura ‘switch-case’

Cuántas veces hemos desarrollado rápido y no nos hemos parado a pensar y hemos acabado escribiendo una estructura parecida a esta:

if (condition) {
    sentence...
    sentence...
    sentence...
} else if (condition2) {
    sentence2...
    sentence2...
    sentence2...
} else if (condition3) {
    sentence3...
    sentence3...
    sentence3...
} else {
    sentence4...
    sentence4...
    sentence4...
}

Esta estructura acaba durando poco en nuestro código y en cuanto podemos lo refactorizamos a algo parecido a esto:

switch (option) {
    case 1:
        sentence...
        sentence...
        sentence...
        break;
    case 2:
        sentence2...
        sentence2...
        sentence2...
        break;
    case 3:
        sentence3...
        sentence3...
        sentence3...
    default:
        sentence4...
        sentence4...
        sentence4...
        break;
}

Si estás un poco metido en JavaScript, sabes que esta estructura es algo cuestionada en la comunidad. Muchos linter permiten activar una opción que te indica que estás haciendo uso de una estructura ‘switch-case’.

Y es que si, al problema de lo poco mantenible de este código, unimos el hecho de que, si no ponemos la palabra ‘break’ dentro de un caso sigue evaluando los siguientes casos, tenemos algo que puede ser un coladero de bugs.

Sin ir más lejos, sin querer me he dejado un ‘break’ sin poner :). En este caso, no solo se ejecutarán las sentencias del caso 3, sino que también se ejecutarán todas las del caso por defecto.

De primeras, lo que vamos a hacer es sacar todas esas sentencias a pequeñas funciones para que nuestro código sea más legible:

function doSomething() {
    sentence... 
    sentence... 
    sentence...
}

function doSomething2() {
    sentence2... 
    sentence2... 
    sentence2...
}

function doSomething3() {
    sentence3... 
    sentence3... 
    sentence3...
}

function doSomething4() {
    sentence4... 
    sentence4... 
    sentence4...
}

function doSwitch(option) {
    switch (option) {
        case 1:
            return doSomething();
        case 2:
            return doSomething2();
        case 3:  
            return doSomething3();
        default: 
            return doDefault();
    }
}

El código que tenemos ahora está mejor modularizado y es reutilizable gracias a que hemos separado los diferentes bloques en funciones. Además, hemos eliminado los ‘break’ por los ‘return’ lo que hace que siempre salgamos de la estructura de control.

Si como yo, sois muy tiquismiquis con el código limpio, os olerá un poco el tener tantos ‘returns’ en una función. Bueno, pues podemos mejorar esta estructura con un pequeño truco que va a hacer que ya solo contemos con un solo return y que el comportamiento sea el mismo:

function doSwitch(option) {
    const options = {
        '1': doSomething,
        '2': doSomething2,
        '3': doSomething3,
        'default': doDefault,
    }

    return (options[option] || options['default'])(); 
}

Fijaos que simple y sencilla se queda nuestra estructura. Hemos eliminado todo aquello que tiene que ver con la estructura ‘switch-case’ y se nos ha quedado un código mucho más declarativo.

Lo que hace nuestra función es configurar un objeto donde se guardan las diferentes funciones cuya opción deseamos ejecutar.

Cuando un desarrollador quiere ejecutar cierta opción, simplemente vamos a buscarla con la estructura ‘obj[key]’ que también funciona para objetos. Si encontramos una opción configurada, ejecutamos la función y devolvemos el valor, si no la encuentra, ejecutamos la función por defecto.

Creo que mola bastante.

Cómo crear un diccionario de datos

Y con esta idea anterior, podemos hacer algo muy parecido a un diccionario de datos. Y es que al final un array o un objeto en JavaScript se comporta como un diccionario clave-valor.

¿No os ha pasado muchas veces, que tenéis una colección de usuarios aleatorio, y que necesitáis hacer búsquedas de un usuario en concreto por su ID de usuario? El algoritmo simplón de búsqueda podría ser algo parecido a esto:

const users = [{
    id: '3212-2232-3423-3425',
    name: 'Jose Dongil'
}, {
   id: '4567-3456-3456-7667',
   name: 'Ana María Borlaz'
}];

function getUserById(users, userId) {
    for (let index = 0; index <= users.length; index++) {
        if (users[index].id === userId) {
            return users[index];
        }
    }
}

getUserById(users, '3212-2232-3423-3425'); 
// { id: '3212-2232-3423-3425', name: 'Jose Dongil' }

Me cansa mucho tener que hacer este tipo de bucles donde, por lo general, nuestra lógica empieza a crecer y a ser otro coladero de bugs. Podemos mejorarlo haciéndolo más declarativo con el método find:

const users = [{
    id: '3212-2232-3423-3425',
    name: 'Jose Dongil'
}, {
   id: '4567-3456-3456-7667',
   name: 'Ana María Borlaz'
}];

function getUserById(users, userId) {
    return users.find(user => user.id === userId);
}

getUserById(users, '3212-2232-3423-3425'); 
// { id: '3212-2232-3423-3425', name: 'Jose Dongil' }

Sin embargo, hacemos muchas iteraciones sobre el array. Imaginate que hacemos un uso masivo de consultas y que los resultados siempre se encuentran al final. Creo que podemos mejorarlo.

Lo primero que podemos hacer es convertir nuestro array en un objeto. Esto lo podemos hacer con el método ‘reduce’ de esta manera:

function convertArrayToObject(array) {
    return array.reduce(function (obj, item) { 
        obj[item.id] = item; 
        return obj; 
    }, {});
}

const dUsers = convertArrayToObject(users);

Lo que obtenemos es un objeto cuyas ‘keys’ son los ids de los usuarios. De esta forma, la búsqueda se convierte en algo automático y sencillo:

dUsers['3212-2232-3423-3425'];

Si necesito guardar un nuevo usuario, también es bastante sencillo:

if  (dUsers) {
    dUsers[newUser.id] = newUser;
}

El ejemplo quizá sea algo chorra, pero para realizar ciertos sistemas de cacheo, puede ser bastante útil.

Cómo componer objetos sin la necesidad de clases

Desde que ha salido ES6, contamos con una nueva forma de declarar clases en JavaScript. Yo ahora puedo definir una clase de la siguiente forma:

class Polygon {
    constructor(width, height) {
        this.width = width;
        this.height = height;
    }

    calculateArea() {
    }

    calculatePerimeter() {
    }
}

Al final esto es azúcar sintáctico de los objetos prototípicos de toda la vida. Podemos hacer herencia con esta nueva estructura de la siguiente forma:

class Square extends Polygon {
    constructor(length) {
        super(length, length);
        this.length = length;
    }

    toString() {
    }
}

Bueno, ES6 viene con una forma de componer objetos que quizá sea más liviana en muchas ocasiones. Se trata de apoyarnos en el método ‘Object.assign’. Este método permite que incluyamos dos objetos y que se compongan. Hay que tener cuidado con él, porque el clonado de propiedades no es profunda.

Cómo componer funciones gracias a RambdaJS

Empezamos a refactorizar. Sacamos funciones muy pequeñitas, una única responsabilidad, una entrada y una salida. Todo perfecto. Y de repente nos encontramos con esto:

doSomething(doSomething2(doSomething3(doSomething4('2122-1232-2454-2467'))));

Hemos conseguido modularizar y crear código reutilizable, pero hemos acabado con una estructura que es poco manejable: demasiado paréntesis, demasiado ofuscado para mi gusto.

Podríamos hacer algo para que se lea mejor, pero el resultado me parece terrorífico:

doSomething(
    doSomething2(
        doSomething3(
            doSomething4('2122-1232-2454-2467')
        )
    )
);

No se ni como llamar a esto la verdad. Yo me lo he encontrado. Pero me da algo de grima.

Para leer este código mejor. Sería bueno que pudiésemos componer funciones a partir de otras. Es lo que comentábamos aquí. Si usamos RambdaJS, tenemos dos métodos para convertir esta estructura en algo más cómodo: ‘R.pipe’ y ‘R.compose’:

const fnCompose = R.pipe(
    doSomethin4, 
    doSomething3,
    doSomething2,
    doSomething
);

fnCompose('2122-1232-2454-2467');

Lo que hace pipe es ir ejecutando todas las funciones de forma encadenada, la salida de ‘doSomething4’ es la entrada de ‘doSomething3’ y así. El código queda mucho más legible y se ve cómo son los pasos que se van a ejecutar.

La librería no es solo útil para esto, así que os recomiendo que le deis una oportunidad, os quedará un código muchos más declarativo gracias a ella.

Cómo evitar el problema del ‘promise callback hell’

Siempre intentamos evitar el uso de los ‘callbacks hell’, esa estructura horrible que nos hace indentar código hasta el abismo de la pantalla. Ese flujo que nos lleva de operación asíncrona a operación asíncrona. Si a alguno no le suena, esto le hará recordar la memoria:

function doSomething(cb1) {
    doSomethingAsync(function (value1) {
        doSomethingAsync2(value1, function (value2) {
             doSomethingAsync3(value2, function (value3) {
                 cb1(value3);
             });
        });
    });
}

Nos prometieron que con las promesas íbamos a acabar con esta desdicha, sin embargo, todavía hay muchas veces en la que es fácil encontrarse con este código:

function doSomething(c1) {
    doSomethingAsync().then(function (value1) {
        doSomethingAsync2(value1).then(function (value2) {
             doSomethingAsync3(value2).then(function (value3) {
                 cb1(value3);
             });
        });
    });
}

Parece que si usamos las promesas de esta manera, seguimos teniendo el mismo problema de indentación en el código. Puede parecer trivial, pero sí, una promesa es capaz de concatenar cómputos, por lo que yo podría hacer algo parecido a esto con la función anterior y obtendríamos el mismo valor:

function doSomething(cb1) {
    doSomethingAsync()
        .then(doSomethingAsync2)
        .then(doSomethingAsync3)
        .then(cb1);
}

La clave de las promesas es esta: indentar menos, modularizar más y obtener un código declarativo. Si vamos a tener el mismo callback que antes, las promesas no nos aportan valor.

Cómo puedes separar la cabeza del cuerpo de un array

Esto me encanta de ES6. Teniendo este array:

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];

Yo puedo hacer esto:

const [head, ...tail] = numbers;

head; // 1
tail; // [2, 3, 4, 5, 6, 7, 8, 9];

No hay más. Tontísimo, pero muy de mi agrado.

Pero es que el truquito, nos puede venir muy bien, para concatenar dos arrays por ejemplo:

const arrayConcat = [...tail, ...tail];

Es una de las ventajas de usar la desestructuración de objetos y arrays con el operador de propagación.

Cómo puedes crear funciones genéricas

Y es que el operación de propagación no solo nos viene bien para esto. Imaginemos que necesitamos desarrollar algún tipo de función genérica. Queremos que dados unos parámetros y una función se ejecute esta función con estos parámetros.

Imaginemos una calculadora. Quiero que se me indique como primer argumento la función a ejecutar y como siguientes argumentos infinitos números sobre los que realizar la operación. En JavaScript no podemos hacer algo como esto:

function calculator(operation, num1, num2, num3) {
    return operation(num1, num2, num3);
}

Bueno si, podemos hacerlo, pero no es genérico. Es una calculadora que solo permite operar sobre 3 números dados.

Una solución en ES5 suele ser la siguiente:

function calculator() {
    const args = Array.prototype.slice.call(arguments);
    const operation = args[0];
    const numbers = args.slice(1, args.length)

    return operation.apply(null, numbers);
}

function add() {
    return Array.prototype.slice.call(arguments)
        .reduce((total, num) => total + num, 0);
}

calculator(add, 1, 3, 4, 5, 6, 7);

Pues si usamos las mismas nuevas técnicas de ES6, podemos tener esta forma mucho más sencilla. Veamos como:

function calculator(operation, ...numbers) {
    return operation(...numbers);
}

function add(...numbers) {
    return numbers.reduce((total, num) => total + num, 0);
}

calculator(add, 1, 3, 4, 5, 6, 7);

Si no es mágico, cerca se le queda.

Cómo puedes acceder a propiedades profundas con un Proxy

Nos pasa muchas veces que tenemos una estructura como esta:

const user = {
    name : 'Jose',
    mother : {
      name : 'Macarena'
    }
}

Y cuando queremos acceder a la propiedad ‘user.father.name’ nos devuelve el típico error:

// Throws the error: Uncaught TypeError: Cannot read property ‘name’ of undefined

Como también hemos comentado en la serie funcional, a veces es importante evitar estas excepciones tan abrupta que rompen nuestra cadena de ejecución y poder delegar el error a funciones padre.

Desde ES6 podemos usar los Proxies para poder modificar el comportamiento de de los ‘getters’ y los ‘setters’ de un objeto. Podríamos generar una especie de mónada ‘Either’ que fuese delegando un objeto de tipo ‘Undefined’ si la propiedad padre no existe.

Esto se puede hacer de esta manera:

const isObject = obj => obj && typeof obj === 'object';
const hasKey = (obj, key) => key in obj;
const Undefined = new Proxy({}, {
    get: function(target, name){
        return Undefined;
    }
 });

function safe(obj) {
    return new Proxy(obj, {
        get: function(target, name) {
            return hasKey(target, name) ? 
                (isObject(target[name]) ? 
                   safe(target[name]) : target[name]) : Undefined;
       }
    });
}

La función ‘safe’ envuelve un objeto en nuestro proxy seguro. Si observáis detenidamente, hay recursividad para llegar a todos los objetos profundos de la estructura. Cuando se accede a una propiedad, se comprueba si está vacía y de ser así se devuelve una estructura ‘Undefined’ que es otro proxy que se encarga de ir propagandose en toda su profundidad. De esta forma evitamos el ‘TypeError’.

Ahora ya podemos hacer esto:

const userSafe = safe(user);

Y ejecutar esto:

console.log(userSafe.name); // devuelve "Jose"
console.log(userSafe.mother.name); // devuelve "Macarena"
console.log(userSafe.father.name); // devuelve una referencia a Undefined

Cómo puedes realizar llamadas asíncronas paralelas con AsyncJS

Muchas veces, para ganar tiempos, necesitamos realizar varias llamadas a una API y podemos hacerlo en paralelo. Muchas veces liamos algoritmos complejos para conseguir esto. Ir ejecutando todo con recursividad e ir controlando diferentes callbacks o promesas.

Aunque todas las librerías de gestión de asincronismo ya cuenta con esta funcionalidad, yo os recomiendo ‘AsyncJS’. Podriamos gestionar funciones asyncronas de la syguiente manera:

function doSomethingAsync(callback) { 
    setTimeout(function() { 
        callback(null, 'one'); 
    }, 200); 
}

function doSomethingAsync2(callback) { 
    setTimeout(function() { 
        callback(null, 'two'); 
    }, 100); 
}

// optional callback 
function doSomethingCb(err, results) { 
    // the results array will equal ['one','two'] even though 
    // the second function had a shorter timeout. 
}

async.parallel([ 
   doSomethingAsync, 
   doSomethingAsync2 
], doSomethingCb);

Queda limpio, mantenible, testeable y reutilizable.

Cómo puedes depurar mejor la pila de llamadas

Volvamos a este código:

function doSomething(cb1) {
    doSomethingAsync(function (value1) {
        doSomethingAsync2(value1, function (value2) {
             doSomethingAsync3(value2, function (value3) {
                 cb1(null, value3);
             });
        });
    });
}

Esas funciones anónimas son dificiles de depurar y testear. Cuando encontramos un error en esa pila de ejecución, solemos encontrar en el Developer Tools un monton de funciones anónimas que nos es difícil seguir.

Si queremos que, que la pile registre una mejor trazada de quien ha ejecutado a quien: Sería mejor que hiciésemos esto:

function doSomething(cb1) {
    doSomethingAsync(function doSomethingAsyncCb(value1) {
        doSomethingAsync2(value1, function doSomethingAsync2Cb(value2) {
             doSomethingAsync3(value2, function doSomethingAsync3Cb(value3) {
                 cb1(null, value3);
             });
        });
    });
}

Sigue siendo una auténtica ñapa pero por lo menos vamos a ganar en tiempo de depuración. Que no en tiempo de mantenimiento. Si ya tenemos esto, ¿Que nos cuesta sacar las funciones fuera:

function doSomething(cb1) {
    doSomethingAsync(doSomethingAsyncCb);
    
    function doSomethingAsyncCb(value1) { 
        doSomethingAsync2(value1, doSomethingAsync2Cb); 
    }
 
    function doSomethingAsync2Cb(value2) { 
        doSomethingAsync3(value2, cb1); 
    }
}

Sigue siendo feo, está muy ofuscado y es poco reutilizable, pero por lo menos hemos quitado la indentación, hemos conseguido un proceso de acción-reacción (una función doSomethig* acaba accionando una función callback).

Podemos mejorar lo siguiente con la librería de AsyncJS y su método ‘Waterfall’ de esta manera:

function doSomething(cb1) {
    async.waterfall([
        doSomethingAsync,
        doSomethingAsync2,
        doSomethingAsync3,
   ], cb1);
}

Conclusión

Espero que os haya sido útil el post y que hayáis podido aprender algo. No me considero un ninja JavaScript, pero muchas de estas cosas, me han acabado ayudando a hacer un mejor código.

Si alguno de vosotros tiene algún truco que compartir, que no dude en exponerlo en los comentarios y escribir sus propias propuestas en blogs. Lo ideal es que compartamos entre todos.

Nos leemos 🙂

Anuncios

5 comments

  1. panicoenlaxbox · 9 Days Ago

    Hola Jose,

    Muy bueno el post, pero es de lectura lenta, me niego a leerlo y no hacer pruebas 🙂

    Estoy con la primera y me pregunto, ¿no sería mejor algo así para evitar que cada función tenga que devolver un truly y así no se ejecutara la opción por defecto?

    if(option in options) {
    return options[option]();
    }
    return doDefault();

    Ya se que 2 returns no molan, pero están muy juntos, ahí tienen un pase 🙂

    Sigo con la lectura!

    Me gusta

    • jdonsan · 9 Days Ago

      De hecho mi código genera un fallo por ejecutar una función de undefined. Lo de los returns lo podemos solucionar así

      return (options[option]) ?option[option]() : doDefault();

      Lo cambio. Gracias Sergio! El post ha sido un desastre por no ser ejemplo real y probado, mucha rectificación. Pero bueno, entre todos lo corregimos seguro.

      Me gusta

      • panicoenlaxbox · 9 Days Ago

        Ah, pues mola más, es verdad, sin 2 return y en una sola sentencia.

        Yo tengo intención de probar todo sin tengo este puente libre, no estará todo probado pero me fío de ti 🙂

        Y de desastre nada, se equivoca quien hace cosas, los demás a agradecer el esfuerzo y a sumar en lo que podamos!

        Me gusta

    • jdonsan · 9 Days Ago

      La opción que más me convence es esta:

      return (options[option] || options[‘default’])();

      Aquí no hay fallo. Y la opción default solo se ejecuta si options[option] es undefined el ‘OR’ en JavaScript es ‘minimal evaluation’: https://en.wikipedia.org/wiki/Short-circuit_evaluation. Solo se ejecuta la segunda si no se tiene clara la primera evaluación.

      Le gusta a 1 persona

  2. plastiqman · 8 Days Ago

    Que articulazo!!!!Ahora mismo lo comparto. Enhorabuena !

    Le gusta a 1 persona

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s