De if/switch a mapped functions (a.k.a. object lookup)
Dailos Rafael Díaz Lara
Posted on December 14, 2020
🎯 Objetivo
Este texto está orientado a proporcionar una alternativa para aquellas situaciones donde nuestro código debe estructurarse para ejecutar una u otra función, dependiendo de un conjunto definido de posibles condiciones.
En ningún momento es mi intención criticar el uso de if/else
o de switch/case
. Mi único objetivo es proporcionar una propuesta diferente con que mejorar el mantenimiento y la escalabilidad de nuestro código.
Una vez dicho esto... empecemos!!!
📚 Instrucción if/else
Desde que empezamos a aprender a programar, el primer control de flujo de información que aprendemos es el if/else
(MDN if/else documentation). De este modo, cuando ya lo hemos aprendido, es realmente fácil utilizarlo.
Incluso cuando la cantidad de posibles opciones se incrementa, podemos encadenar multiples if/else
.
Además, cuando tenemos varias opciones que deben tratarse de la misma manera, es decir que comparten la misma lógica de negocio, podemos usar operadores booleanos (el OR
en este caso), para agrupar todas esas opciones bajo el mismo bloque de código.
Todo esto está genial pero cuando los posibles casos superan las dos o tres opciones, el código empieza a parece un poco sucio.
Pros (✅) y contras (👎)
✅ Es la manera más fácil de controlar el flujo de información.
✅ Es relativamente fácil de aprender.
✅ discriminar entre dos posibles opciones es realmente cómodo.
👎 Cuando gestionamos más de tres opciones, el código empieza a parece un poco sucio..
👎 Encadenar múltiples opciones disminuye la legibilidad y el mantenimiento de nuestro código.
👎 Agrupar opciones empleando operadores booleanos puede hacer más complicadas las reglas de comparación en cada situación.
👎 Para una cantidad relativamente grande de casos posibles, es más lento ya que cada condición deber ser comprobada hasta alcanzar aquella que coincida con el valor de referencia proporcionado.
🤓 Instrucción switch/case
Cuando queremos mejorar la legibilidad y mantenimiento de nuestro código debido a que tenemos múltiples opciones que gestionar, es cuando aprendemos la alternativa al if/else
, es decir, el switch/case
(MDN switch/case documentation).
De la misma manera que hacíamos con el if/else
, con el switch/case
también podremos agrupar opciones pero en este caso, no necesitamos utilizar ningún operador booleano. Sólo necesitamos mantener unidos los diferentes casos a agrupar.
Como ya sabrás, esto es posible gracias a que la ejecución del switch/case
es un proceso secuencial, donde cada posible caso definido en el conjunto de opciones, es comparado con la referencia proporcionada.
Si ambos valores coincide, el bloque de código incluido en ese caso se ejecuta y, si no hay una instrucción break
o return
al final de dicho bloque de código, el siguiente caso será comprobado hasta encontrar la próxima coincidencia o hasta llegar al bloque default
.
Basándonos en esto, para agrupar múltiples opciones las cuales van a ser gestionadas por el mismo bloque de código, sólo necesitamos definir el caso para el valor deseado, sin ningún tipo de lógica de negocio. De este modo seremos capaces de encadenar múltiples opciones para el mismo resultado.
Pros (✅) y contras (👎)
✅ Proporciona una mejor estructura del código que al usar instrucciones if/else
.
✅ Es posible crear agrupamiento de casos de una manera más clara que con instrucciones if/else
.
✅ Es realmente sencillo discriminar entre más de dos opciones.
👎 Tenemos que estar pendiente de completar todos los bloques de código con una instrucción break
o return
. Si nos olvidamos de hacerlo, nos podemos meter en un buen lío.
👎 Para cantidades relativamente grandes de casos, es lento dado que cada condición debe ser comprobada hasta llegar a aquella que coincide con la referencia que le hemos proporcionado.
🔥 Mapped functions
Esta es una estrategia poco conocida (también llamada object lookup
) y está destinada a mejorar determinados aspectos del uso de instrucciones if/else
y switch/case
.
La idea es aprovechar el comportamiento de los objetos de JavaScript para usar sus claves como mapa de referencias y acceder directamente a lógica de negocio específica.
Antes de nada, necesitamos tener definidos los posibles casos que van a ser gestionados.
Cada caso individual será asociado a una clave del objeto literal.
Una vez hemos creado nuestro objeto, usaremos el estilo de acceso array para ejecutar el código de cada caso individual.
Pros (✅) y contras (👎)
✅ Proporciona una estructuración del código mejor que la que obtenemos al usar instrucciones if/else
y switch/case
.
✅ No hay agrupamiento de posibles casos dado que cada uno de ellos tiene definida su propia lógica de negocio.
✅ Es extremadamente fácil diferenciar entre múltiples opciones de ejecución.
✅ Puede ser reutilizado en varias partes de nuestra aplicación (via exportación de módulo).
✅ Es más rápido que if/else
y switch/case
dado que accedemos a la condición específica que queremos ejecutar, sin necesitar verificar cada uno de los casos secuencialmente, hasta encontrar el correcto.
👎 Esta estrategia rara vez aparece en las formaciones más habituales.
👎 Si el objeto no se define en el lugar indicado de la aplicación, puede consumir un poco más de memoria de la necesaria.
🤔 FAQ
❓ ¿Qué sucede si proporcionamos una opción que no están entre las claves del objeto?
La respuesta corta es que se disparará una excepción ya que no es posible ejecutar una función de undefined
.
No obstante, podemos prevenir esto definiendo un caso default
, de la misma manera que hacemos en la instrucción switch/case
.
Para ser capaces de acceder a este nuevo caso, comprobaremos si la opción proporcionada existe dentro del objeto y si no existe, entonces ejecutaremos la opción default
.
Para estos casos, operador condicional (ternario) será nuestro aliado.
❓ ¿Qué puedo o debo devolver en el caso default
?
Esto va a depender del caso de uso que estemos definiendo pero básicamente, vamos a tener tres opciones principales:
1 - Devolver el mismo valor que hemos proporcionado:
2 - Devolver null
o undefined
:
En este caso, podemos incluso aprovechar el optional chaining y dejar más limpio nuestro código:
Debemos prestar atención porque in este caso, si no hay coincidencia en las opciones disponibles, vamos a devolver undefined
.
3 - Definir una lógica de negocio específica:
Aquí debemos tener cuidado si nuestro código, como se muestra en el ejemplo, va a disparar un error. Tenemos que gestionar dicho error para evitar un error total que bloquee nuestra aplicación.
Obviamente el código que implementa el error puede ser reemplazado por cualquier otra lógica de negocio que se adecue mejor al comportamiento de nuestra aplicación.
❓ ¿Necesito definir una función anónima para cada caso?
No, en absoluto.
Si tenemos perfectamente definida la función que debe ser ejecutada para cada caso y además, dicha función recibe únicamente un argumento que coincide con el que estamos proporcionando cuando invocamos al mapa, podemos usar esta sintaxis:
Incluso si queremos devolver undefined
cuando la opción proporcionada no está incluida dentro del mapa, podemos usar esta otra sintaxis extremadamente simplificada (Advertencia ‼️: todas las funciones usadas para crear las claves del mapa, han de estar definidas previamente):
❓ ¿Es posible que el nombre de una propiedad entre en conflicto con el de un objeto?
Rotundamente sí.
Es totalmente posible, pero para evitar esto tenemos que prestar atención a los nombres que estamos usando, de la misma manera que nunca utilizaríamos una palabra reservada del lenguaje como nombre de variable, función u objeto.
❓ ¿Esto podría formar una nueva convención de nombres?
Sí, claro.
Pero para este tipo de situaciones tenemos el apoyo y las guías proporcionadas por el Clean Code.
Cada código que creemos requerirá una convención de nombres. En algunos casos cuando seamos la única persona que ha iniciado el proyecto, podremos definir dicha convención (pet-projects principalmente). En otras situaciones, será el equipo de desarrollo el responsable de cualquier acuerdo alcanzado a tal efecto.
❓ ¿Requerirá un uso de memoria adicional mientras que el if/else
y el switch/case
no lo hacen?
Sí, lo hará.
Sin embargo, basándonos en los tipos de dispositivos que ejecutan nuestras aplicaciones JavaScript hoy en día así como en sus características, el incremento de memoria es prácticamente insignificante en comparación con el resto de la aplicación.
❓ ¿Sería esta opción más lenta que el if/else
o el switch/case
dependiendo del motor de JavaScript que se use?
Esto va a depender de cómo definamos el objeto en sí.
Por ejemplo, si definimos el objeto de mapeado de funciones dentro de una función, la cual va a ser invocada cada vez que queramos hacer uso del mapa, obviamente esta solución va a ser más lenta que las otras opciones, porque el objeto debe ser creado cada vez.
En este código podemos ver la situación donde la función mappedFunction
tiene definido el objeto dentro de ella:
Codepen 👉 Speed race Switch 🐇 vs Object Lookup 🐢 v1
Aquí no importa qué motor de JavaScript estemos usando para ejecutar el código (AppleWebKit para Safari, SpiderMonkey para Firefox o V8 para Google Chrome y/o NodeJS), porque el mapeado de funciones será siempre más lento (incluso si operamos los primeros casos), debido a que el objeto se está creando ad-hoc en cada ejecución de la función.
Sin embargo, si definimos el mapeado de funciones de manera global (al módulo o a la aplicación), el objeto se cargará cuando el módulo o la aplicación lo usen. De este modo, el acceso a las funciones mapeadas será siempre más rápido que las otras dos opciones.
En este código hemos definido el mapa fuera de la función mappedFunction
:
Codepen 👉 Speed race Switch 🐢 vs Object Lookup 🐇 v2
❓ ¿Qué pasa con el recolector de basura?
Hoy en día el recolector de basura es algo a lo que quienes desarrollamos con JavaScript no le prestamos mucha atención, debido a que está ampliamente cubierto por las especificaciones del lenguaje así que, una vez el mapa de funciones ya no está en uso en el proceso de ejecución actual, el objeto será gestionado por el recolector de basura automáticamente.
Para más información respecto a este tema, recomiendo echar un vistazo a esta documentación de la MDN relativa a la gestión de la memoria.
Recursos adicionales:
👋 Conclusiones finales
Como ya he dicho al principio de este post, no es mi intención criticar de ningún modo el uso de if/else
o switch/case
, sino que únicamente pretendo proporcionar otra manera de realizar dichas operaciones.
Resumiendo, cuando tengamos que discriminar entre dos simples opciones, es obvio que la alternativa más sencilla es usar if/else
. Además recomiendo encarecidamente que intentes usar el operador ternario allí donde sea posible.
Para aquellos casos donde tengamos que diferenciar entre tres o más opciones, sinceramente recomiendo el uso de funciones mapeadas para proporcionar una mejor legibilidad, mantenimiento y reutilización de nuestro código.
Espero que este contenido te sea útil. Si tienes cualquier pregunta, siéntete totalmente libre de contactar conmigo. Aquí están mis perfiles de Twitter, Linkedin y Github.
🙏 Reconocimientos y agradecimientos
- A Lissette Luis, Adrián Ferrera e Iván Bacallado por formar parte de un equipo fantástico donde se comparte el conocimiento y especialmente, por sus propuestas sobre el tema abordado en este texto.
- A Simon Høiberg por iniciar este interesantísimo hilo de Twitter que ha originado la creación de este post.
- A Kenan Yildiz y Thomas Luzat por compartir una opción más simplificada de la implementación del mapa de funciones.
Posted on December 14, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
October 27, 2024
October 21, 2024