Abstracción: Extraer lo obvio y agregar lo importante
Técnicas de abstracción extrapoladas a Javascript
Las abstracciones son necesarias para el correcto diseño de cualquier software a prueba de balas. Nos ayudan a pensar y a trasladar los conceptos que nos rodean al plano fisico, permitiendo que nos antepongamos de forma más o menos certera ante posibles futuros cambios. Además son una estrategia habitual de reutilización.
Imaginemos que ante una determinada lista de números (son números ahora pero podrían acabar siendo otra cosa) queremos mostrarlos por la consola, de momento es la consola de desarrolladores pero en el futuro podría ser otra consola o la lavadora de tu casa.
Una compañera ha sido asignada para realizar tan ardua tarea. Ya tiene una primera aproximación:
Ha definido una función showNumbers
que ante una lista de numeros los va mostrando uno a uno por consola. La implementación aunque no es demasiado elegante, es funcional — en principio —.
Le mete los tests — lo debería haber hecho antes siguiendo las enseñanzas de TDD pero nadie le ha visto —, empaqueta y sube a producción. Los stakeholders parecen complacidos.
Al día siguiente nos comentan que el programa está funcionando tan bién que quieren extrapolar a mostrar otros tipos de datos, como String.
Nuestra amiga tiene un buen día e instantaneamente ve la solución: como Javascript es una lenguaje de tipado débil realmente no tiene que cambiar nada, tan solo actualizar la nomenclatura ya que ahora las reglas de negocio no va de “mostrar números”:
Todo solucionado y no son ni las 9 y media. Merecido descanso.
Estaba todo marchando bien hasta que le comentan que ayer en la reunión de última hora llegaron a la conclusión de que los números impares no aportaban nada al usuario y que solo los pares parecian suscitar en ellos algún tipo de utilidad.
Aquí nuestra programadora se encuentra una dicotomía:
O cambia la función original para que en el caso de que los valores sean números, solo imprima el par. Sin embargo tendría que recurrir a checkeos de tipos (un poco contraproducente en Javascript) y tendría que pensar un mejor nombre para la función.
O hace dos funciones, una para mostrar numeros y otra strings.
Al final y después de darse cabezazos contra la pared buscando un nombre bonito para una función que itera, logea y mira si un numero es par o no, opta por seguir la segunda opción aunque está implique duplicar código, por qué son solo 5 lineas de nada no?:
Ha tenido que deshacer los cambios de nombre de esta mañana y ha acabado con 2 funciones quasi identicas. Aún así, como todo parece funcionar después de duplicar los tests (si se duplica la implementación los tests van detrás) decide subir a producción y bajar a la cafetería a por otro café.
Todavía no ha terminado de beberselo cuando le abren una incidencia porque al parecer algunos usuarios están reportando en sus consolas algunos valores undefined.
Revisa el código y los tests y se da cuenta de que el código original tenía un bug. En concreto la condición de parada del bucle for no es correcta. Y claro cuando ha copiado y pegado el código de la función original, ha copiado el bug también.
Este es uno de los mayores riesgos cuando se copia y se pega código. Si había bugs los estaras copiando también. El día que tengas que arreglarlo tienes que tocar en 2 sitios.
Ya más tranquila (o más nerviosa) y acordandose de lo que ha leido numerosas veces en el rincón del front sobre que hay que hacerse preguntas siempre, se dice para si:
Hay una mejor forma de hacer esto?
Analicemos el problema con estas dos funciones.
El primero salta a la vista: Duplicidad. El segundo no es tan explicito ya que radica en la violación de un principio SOLID: el de única responsabilidad.
Vamos a ir evolucionando junto con nuestra compañera a que nos referimos con única responsabilidad formulando otra pregunta:
Cuál es la responsabilidad de showNumbers/showString?
Si atendemos a su nombre, es sencillo: mostrar numeros o cadenas respectivamente. Pero si miramos más profundamente a los detalles de implementación nos encontramos muchas más resposabilidades ocultas:
La de recorrer un Array
La de efectuar lógica de flujo (en el caso de showNumbers)
La de realizar una operación sobre el valor (mostrarlo por consola)
— Paralelamente mientras analizamos el caso, llega otra tarea por email que urge a nuestra amiga a concatenar delante del valor a mostrar la cadena “log :”, así deberiamos ir mostrando “log: 2”, “log: 4”, “log: hola”… —
Hace tan solo 1 hora que hemos duplicado el código y ya están saliendo más duplicidades. Una decisión rápida y sin mucha meditación está apunto de convertirse en una decisión catastrofica.
Abstrayendo lo obvio
En el libro The laws of simplicity, John Maeda afirma lo siguiente con respecto al concepto de Simplicity:
“Simplicity is about subtracting the obvious and adding the meaningful”
Y hasta donde yo sé fue Eric Elliot quien extrapoló este concepto a la abstracción afirmando que “la abstracción es extraer lo obvio y agregar lo que verdaderamente importa”. O dicho en castellano esquisito: “Que extraigas a funciones lo que se repite y parametrices lo que cambia”. Pura poesía.
Identificando repeticiones
Esta es sencilla porque además es algo que suele chocar bastante a la vista. Volviendo a nuestro ejemplo: estas son las cosas que se repiten después de hacer un revisión rápida:
El function showLoquesea más el parametro. Aunque los nombres sean distintos la definción de la función tiene el mismo esquema, por lo que estamos ante código repetido.
El bucle for. En caso de
showNumbers
se itera secuencialmente (desde el principio hasta el final) sobre un array de numbers y en caso deshowString
sobre uno de strings. Como en Javascript un Array de Strings es lo mismo que un Array de Numbers. El código es identico.De hecho vamos más allá. No solo se reptie el bucle for si no también sus componentes. Esto es: la incialización del contador (i), la condicción de iteración (i < numbers/strings length) y la actualización (incrementar el valor de i)
Dentro del bucle for ya empezamos a ver más diferencias — no tanto sintácticas, si no de flujo —, en un caso se filtra antes de logear y en otros se logea directamente. Esta es la parte “meaningful” a la que se refería maeda. El resto, el código obvio que se repite podemos abstraerlo para reutilizarlo. Vamos a crear la función forEach
:
function forEach () {
for(var i = 0; i <= numbers.length; i++) {
console.log(numbers[i]);
}
}
De entrada nos hemos llevado todo el código de una de las dos funciones. De aquí ahora vamos a proceder a parametrizar el código que cambia, dejando solo abstraido el código repetido. Para ello tenemos que fijarnos en la información que va a ser diferente cada vez que iteremos sobre una lista:
function forEach (arr, cb) {
for(var i = 0; i <= arr.length; i++) {
cb(arr[i]);
}
}
Voilà!
Echemos un vistazo ahora a como queda nuestro programa utilizando nuestra nueva abstracción forEach
en lugar de tener que reporgramar un bucle for secuencial una y otra vez:
Bien. Adiós a ese feo bucle for, ahora solo tenemos que verlo una vez. Y si nos lo llevamos a un módulo no tenemos que volver a saber nada de esos detalles de implementación nunca más.
Vamos también a aprovechar para solucionar el bug que nos habían reportado antes. Como ahora todo lo que acontece al bucle está abstraido en una función, el arreglo es trivial:
Simplemente hemos eliminado el = extra de la condicción. Como los indices de los arrays empiezan en 0, en una iteración secuencial desde el inicio el ultimo elemento es siempre length - 1
Abstraer es un proceso iterativo
Sin embargo, con este nuevo enfoque hemos vuelto a introducir más código repetido y todavía nos quedan features que implementar (concatenar la cadena “log: “ delante de las impresiones).
Si volvemos a ver el código vemos que en las funciones sigue existiendo duplicidad:
Y si me apuras hasta las cabeceras de las funciones — a excepción de los nombres — son muy parecidas.
Ains!, si no tuvieramos que hacer la condición de si es par o no en showNumbers
, podríamos tener una única función showThings
— como propuso nuestra compañera al principio—, el problema es que la vida es dura y esta tiene una extraña atracción a vernos sufrir.
Antes hemos comentado que la abstracción es eliminar el código repetido y parametrizarlo. No obstante, en este caso el código es lo suficiente diferente como para no poder aplicar esa regla tan a la ligera.
No obstante, al igual que en matematicas cuando algo no tiene existencia real, se lo inventan, nosotros podemos “inventarnos” también la repetición. Es decir imagina que nuestra función showStrings fuese:
function showStrings(strings) {
forEach(strings, str => {
if (true) {
console.log(str);
}
});
}
Ahora con este truco si que el código se repetite exactamente ya que ambas filtran y transforman y además de la misma manera!.
Procedemos a aplicar lo anteriormente visto para extraer lo repetido, parametrizarlo y agregar lo que verdaderamente cambia (el predicado). Además aprovechamos para agregar el comportamiento que nos faltaba:
Como solución no está mal realmente. El único problema es hemos estandarizado la complejidad y ahora se necesite o no, siempre hacemos una condición — aunque tengamos la certeza que para un tipo de array en concreto nunca se va a querer realizar comprobación —.
Delegar responsabilidades como ténica de abstracción
El problema que tenemos delante y el que nos está molestando a la hora de crear una solución universal para cada caso, es que se estamos mezclando filtrado y transformación en un mismo paso.
A veces hay que buscar un equilibrio entre abstracción y duplicación y como habrás podido ver, hasta con los ejemplos más sencillos puede ser difícil tomar una decisión.
Existe una forma al más puro estilo funcional de crear una abstracción un poco más genérica sin renunciar a la reusabilidad y minimizando la duplicidad. Pero hoy vamos a explorar otras técnicas quizá más convencionales.
Si miramos atentamente la función showThings
y volvemos a hacernos la pregunta de “cuáles son sus responsabilidades?” la respuesta de nuevo irá mas en la línea de responsabilidades multiples que unarias:
Por cada elemento de una lista de “cosas”
Se aplica la función predicate
Se genera un mensaje compuesto por la subcadena “log: “ más el nombre de la “cosa”
Se muestra por consola
Muchas cosas para una función tan pequeña. Y al estar estas responsabilidades mezcladas en una función genérica se plantea la cuestión de que un cambio afecta a todo. Ejemplo: mañana nos piden que los numeros impares que no estamos enseñando al usuario, lo guardemos en un fichero. Más complejidad y nuestra solución genérica se vuelve romper :(.
Si observamos el problema en su conjunto podemos vislumbrar quien es el ente que sabe en todo momento como ha de mostrarse al usuario. O dicho de otro modo, conoce la lógica asociada a su razón de ser. Este ente no es otro que la propia “cosa”.
Vamos a ponernos por un momento en la piel de un número — en el contexto de nuestro programa —. Ahora, siendo un numero sabemos cual es nuestra lógica asociada:
Si soy par he de mostrarme por consola.
Si soy impar debo guardarme en un fichero.
Por tanto si somos capaces de representar cada “cosa” y delegarles esas lógicas, habremos conseguido abstraer los flujos y podremos de nuevo pensar en una solución más genérica pero no por ello menos potente.
Definamos por tatno cada “thing”:
const NumberThing = (value) => ({
});
const StringThing = (value) => ({
});
NumberThing
yStringThing
son factorías.
La importacia de exponer interfaces no implementaciones
Todavía escucho a muchos programadores de Javascript afirmar cosas como:
Javascript no es un lenguaje orientado a objetos completo. Porque no tiene clases ni interfaces.
Que no haya una palabra reservada “interface” no hace al lenguaje menos orientado a objetos ni implica que el concepto no exista. De nuevo, es importante conocer los pilares por los que se rigen las herramientas que proporcionan los lenguajes, así podremos extrapolar más facilmente todo.
Una interface es un contrato — o también llamado protocolo — que define generalmente qué métodos ha de haber y como se han de comportar dentro de un objeto. El lenguaje por tanto puede ofrecerte un mecanismo para realizar esto o no, pero no quiere decir que no se puedan aplicar los conceptos.
Así pues, podemos definir que para todas nuestras “things” debe de existir siempre un método run
que ejecuta la lógica asociada a esa “thing”:
function showThings(things) {
forEach(things, thing => {
thing.run();
});
}
Para que este código no expolote “thing” ha de cumplir el contrato anteriormente definido. ¡Es decir, que la “thing” tenga un método run!
Esto es lo que se conoce como duck typing. Donde no realizamos comprobaciones de tipos perse, sino que esperamos que si algo parece y se mueve como un pato, supongamos que lo sea. En nuestro caso, me da un poco igual que tipo de “cosa” sea, tiene que tener un método run.
Por último vamos a implementar este método en NumberThing
y StringThing
:
Hemos representado cada una de nuestros entes y le hemos delegado la responsabilidad del como tratar la información que ellos mismo representan.
Con este nivel de abstracción podemos ahora recurrir a técnicas de composición para reutilizar todavía más si cabe:
Ahora solo deberiamos representar todas nuestras cosas en terminos de StringThing
o NumberThing
según corresponda.
Conclusión
Los ejemplos pueden parecerte un poco básicos según tu nivel, pero por ello no implica que sean ajenos al mismo tipo de problemas que ocurre en codebase más grandes y complejas. El objetivo del artículo era llevar a la práctica el tema de la abstracción y combinarlo con técnicas de composición en Javascript. Pero además, la idea era plantear preguntas. Muchas veces vamos como un pollo sin cabeza desarrollando y nadie se para 2 minutos, toma aire, mira el código en perspectiva y se hace la famosa pregunta de: Hay alguna forma mejor de hacer esto?.
En futuras entregas iremos profundizando en otras técnicas que nos permitan disponer de un buen arsenal de herramientas la próxima vez que nos formulemos esa famosa pregunta.
Nada más por hoy :).
PD: Hay algún ejemplo que no te ha gustado?, lo harías mejor? o encuentras problemas que no he comentado?, déjamelos abajo en un comentario.