Introducción
Los principios de la ingeniería de software, del libro de Robert C.
Martin Clean Code, adaptado para JavaScript. Esta no es una guía de estilo,
en cambio, es una guía para crear software que sea reutilizable,
comprensible y que se pueda mejorar con el tiempo.
No hay que seguir tan estrictamente todos los principios en este libro, y vale
la pena mencionar que hacia muchos de ellos habrá controversia en cuanto
al consentimiento. Estas son reflexiones hechas después de muchos años
de experiencia colectiva de los autores de Clean Code.
Nuestra obra de ingeniería de software lleva poco más que 50 años como
negocio, y aún estamos aprendiendo. Cuando la arquitectura de software
llegue a ser tan vieja como la arquitectura en sí misma, quizás tengamos
reglas más estrictas para seguir. Hasta entonces, dejemos que estas guías
sirvan como ejemplo para medir la calidad del código en JavaScript que tú y
tu equipo producen.
Una cosa más: saber esto no te hará un mejor ingeniero inmediatamente, y
tampoco trabajar con estas herramientas durante muchos años garantiza
que nunca te equivocarás. Cualquier código empieza primero como un
borrador, como arcilla mojada moldeándose en su forma final. Por último,
arreglamos las imperfecciones cuando lo repasamos con nuestros
compañeros de trabajo. No seas tan duro contigo mismo por los borradores
iniciales que aún necesitan mejorar. ¡Trabaja más duro para mejorar el
programa!
Funciones
Funciones
Argumentos de funciones (2 o menos idealmente)
Limitar la cantidad de parámetros de tus funciones es increíblemente
importante ya que hace que tus pruebas del código sean más fáciles. Al
pasar los 3 argumentos, llegarás a un escenario de una explosión
combinatoria en que hay que comprobar con pruebas muchos casos únicos
con un argumento separado.
Uno o dos argumentos es la situación ideal, y más que eso uno debe evitar
si es posible. Todo lo que se puede consolidar se debe consolidar.
Normalmente, si tienes más que dos argumentos, tu función sirve para
hacer demasiado. En otros casos, es mejor refactorizar y hacerlo un objeto
para encapsular las funciones extras.
Ya que JavaScript te deja crear objetos cuando quieras sin incorporar la
arquitectura de 'clases', se puede usar un objeto si necesitas muchos
argumentos.
Para hacerlo más obvio cuáles argumentos espera la función, se puede usar
la sintaxis de ES2015/ES6: 'destructuración'. Esta sintaxis tiene varias
ventajas:
1. Cuando alguien se fija en el firme de la función, es inmediatamente
claro cuáles argumentos se usan.
2. Destructurar también copia los valores específicos y primitivos del
objeto argumento que se le pasa a la función. Esto puede evitar los
efectos extras. Ojo: objetos y arreglos que se destructuran del objeto
argumento NO se copian.
3. Los 'linters' te pueden avisar cuales argumentos / propiedades no se
usan, lo cual sería imposible sin destructurar.
Las funciones deben tener una sola responsabilidad
Esta regla por mucho es la más importante en la ingeniería de software.
Cuando las funciones sirven para hacer más que una sola cosa, se dificultan
las pruebas, la composición y el entender. Cuando puedes aislar una función
hasta tener solo una acción, se pueden mejorar más fácil y tu código llegue
a ser mucho más limpio. Si solamente entiendes una cosa de esta guía,
entiende esta regla y estarás adelantado de muchos desarrolladores.
Las funciones deben tener solo un nivel de abstracción
Cuando tienes más que un nivel de abstracción tu función suele servir para
hacer demasiado. Crear varias funciones más pequeñas se debe a mejor
reutilización y comprobación más fácil.
Eliminar el código duplicado
Haz tanto como puedas para evitar código duplicado. El código duplicado es
malo ya que significa que hay varios lugares donde hay que actualizar algo
si un cambio es necesario en tu lógico.
Imagínate que estás en un restaurante y necesitas organizar tu inventario:
todos tus tomates, cebolla, pimientos y tal. Si tienes varias listas donde
organizas el inventario, cada lista se tendrá que actualizar en cuanto se baja
tu inventario. En cambio, si logras tener una sola lista, solo se actualizará en
un lugar a la hora de apuntar el inventario.
Muchas veces tienes código duplicado se debe al hecho de tener dos o más
cosas semejantes. Estos archivos pueden comparten varias cosas, pero sus
diferencias te obligan separarlos para tener dos o más funciones que hacen
cosas muy similares. Remover el código duplicado significa que se puede
hacer la misma cosa que un solo función/módulo/clase.
Obtener la abstracción correcta es crítica y por eso debes de adherir a los
principios de SOLID que se explican en la sección de Clases. Las malas
abstracciones pueden ser aún peores que el código duplicado, ¡así que ten
cuidado! Es decir, si puedes hacer una buena abstracción, ¡hazla! No te
repitas, si no te darás cuenta de que andas actualizando mucho código en
varios lugares a la hora de implementar un cambio.
Mal hecho
No utilices 'marcadores' como parámetros de las funciones
Los marcadores existen para decirle a tu usuario que esta función hace más
que una sola cosa. Como se ha mencionado antes las funciones deben hacer
una sola cosa. Divide tus funciones en varias funciones más pequeñas si se
adhieren a distintos métodos basados en un booleano.
Evitar que las funciones produzcan efectos extras (parte 1)
Una función produce un efecto extra si hace cualquier cosa más que solo
tomar un valor y volverlo/los). Un efecto extra podría ser escribir a un
archivo, modificar un variable global, o accidentalmente enviar todo tu
dinero a un desconfiado.
Bueno, las funciones necesitan tener efectos extras a menudo. Como el
ejemplo anterior, puede que sea necesario escribir hasta un archivo. En ese
caso, hay que centralizar en el 'por qué' de lo que estás haciendo. No
tengas varias funciones y clases que escriben hasta un archivo particular.
En cambio, crea un 'servicio' que se dedica a eso: uno y solo un servicio.
El punto clave aquí es evitar las equivocaciones comunes como compartir
'estado' entre objeto sin ninguna estructura, utilizar tipos de data mutables
que se pueden escribir hasta lo que sea, y no centralizar donde se ocurren
los efectos extras. Si puedes conseguir esto, serás más feliz que la mayoría
de los demás programadores.
Evitar los efectos extras(parte 2)
En JavaScript, los primitivos se pasan por valores y los objetos/arrays se
pasan por referencia. En el caso de los objetos y los array, si tu función hace
un cambio en la shopping cart array, por ejemplo, con agregar una cosa a la
hora de comprar, resulta que todas las demás funciones que utilizan este
array estarán afectadas. Esto puede ser bueno o malo. Imaginemos una
situación mala:
El usuario le da click a "Comprar", un botón que invoca la función de
"comprar". Esta función hace una solicitud del red y envía el array de 'cart'
hasta el servidor. Debido a la conexión mala del red, la función sigue
intentando invocarse para mandar la solicitud. Ahora, que pasa mientras
tanto cuando el usuario le da click otra vez al botón en una cosa que no
querían antes de que empezase la solicitud del red? Bueno, si pasa eso y
comienza la solicitud del red, la función de 'comprar' mandara sin querer la
cosa que estaba agregada accidentalmente ya que tiene una referencia al
array de 'shopping cart' que la función 'addItemToCart' modifico con
agregar una cosa no deseada.
Una buena solución seria que la función 'addItemToCart' siempre copiara la
'carta', editarla, y devolvérsela a la copia. Esto asegura que ninguna otra
función relacionada se afectará por estos cambios.
Dos cosas para mencionar con esta solución:
1. Puede que existan escenarios donde de verdad quieres modificar el objeto
de input, pero cuando adoptas esta práctica de programar, te darás cuentas
de que estos casos son bastante únicos.
2. Copiar objetos grandes pueden ser muy caros en cuanto a la velocidad y
calidad de tu programa. Afortunadamente, no hay mucho problema con esto
ya que existen muchas recursos que nos dejan lograr el copiar de objetos y
arrays sin perder actuación.
No intentes cambiar las funciones globales
Polucionar las construcciones globales no es buen costumbre en JavaScript
ya que se puede afrontar con otra biblioteca y el usuario de tu API no se
daría cuenta hasta que reciba una excepción cuando ya está en producción
el código. Pensemos en un ejemplo: que pasaría si quisieras extender los
métodos nativos de la clase Array para tener un método de 'diff' en que se
podría mostrar la diferencia entre dos arrays? Podrías escribir una nueva
función hasta el prototipo del [Link], pero eso también podría
causar problemas con otra biblioteca que contenía el método igual. Bueno,
¿qué pasaría si la otra biblioteca solamente usaba ‘diff’ para averiguar la
diferencia entre el primer elemento y el último elemento del array? Por eso
hay que utilizar las clases de ES2015/ES6 y extender el global de Array.
Favorece a la programación funcional en vez de la
programación imperativa
JavaScript no es un idioma funcional tal como es Haskell, pero tiene su
propio sabor funcional. Los idiomas funcionales son más limpios y fáciles de
comprobar. Favorece este estilo de programar cuando puedes.
Evitar los condicionales
Esto parece ser un reto imposible. Al escuchar esto por primera vez, la
mayoría de la gente dirá: "como se supone que hago sin una declaración de
'if'?" Bueno, la respuesta es que puedes utilizar para lograr los mismos retos
en muchos escenarios. La segunda pregunta suele ser: "bueno, eso está
bien, pero por qué voy a querer hacer eso?" La respuesta yace en un
concepto anterior que ya hemos aprendido: una función solo debe hacer
una sola cosa. Cuando tienes clases y funciones que contienen
declaraciones de if, le dices al usuario que tu función hace más que una
sola cosa. Recuerda, solo haz una cosa.
Evitar la comprobación de tipos (parte 1)
JavaScript es un idioma no tecleado, por lo cual significa que tus funciones
pueden aceptar cualquier tipo de argumento. A veces te aprovechas de esta
libertad y tienes ganas de hacer comprobación de tipos dentro de tus
funciones. Hay muchas maneras de evitar tener que hacer esto. La primeras
cosas para considerar son APIs consistentes.
Evitar la comprobación de tipos (parte 2)
Si estás trabajando con los valores primitivos básicos como strings, enteros
y arrays y que no puedes utilizar polimorfismo, pero existe la necesidad de
comprobar los tipos, debes considerar utilizando TypeScript. Es un
alternativo excelente a JavaScript, y te provee con los tipos estáticos
encima del sintaxis estándar de JavaScript. El problema con comprobar los
tipos en JavaScript es que para hacerlo bien resulta en mucho más verbos
que no vale la pena al lado de la legibilidad disminuida que viene a junto
con esta solución. Intenta mantener limpio tu código de JavaScript, escribe
buenas pruebas, y haz buenas revisiones de código. Eso dicho, haz todo lo
de arriba, pero con TypeScript (por lo cual, como dije, es buen alternativo).
Bien hecho
function combine(val1:number, val2:number):number {
return val1 + val2;
}
Eliminar el código muerto
El código muerto es tan elegante como el código duplicado. No hay razón
para guardarlo. Si no se usa, ¡elimínalo! Aun estará en tu historia del control
versión si de verdad lo necesitas.
Objetos y estructuras de data
Utiliza getters y setters
Utilizar los getters y setters para acceder data dentro de los objetos puede
ser mejor que simplemente buscar una propiedad. "Por qué?" Bueno, aquí te
dejo con una lista desorganizada de las razones:
Cuando quieres hacer algo más allá de acceder una propiedad de
objeto, no tienes qué buscar todos los accesorios en tu programa.
Hace que implementar validación sea más fácil cuando construyes
un set
Encapsula la representación internal
Facilita la incorporación de apuntar errores de acceder y crear
Puedes cargar de manera vaga las propiedades del objeto, digamos
de un servidor por ejemplo
Haz que los objetos tengan miembros privados
Esto se puede lograr con closures (con ES5 y antes)
Clases
Prefiere ES2015/ES6 clases en vez de funciones normales de
ES5
Es muy difícil para obtener una herencia legible de las clases, las
construcción y las definiciones de los métodos para las clases de ES5. Si
necesitas la herencia (y puede que no la vayas a necesitar), entonces
prefiere a las clases de ES2015/ES6. Sin embargo, prefiere funciones
pequeñas hasta que necesites objetos más grandes y complejos.
Utiliza la agregación de métodos
Este modelo es muy útil en JavaScript y puede que lo veas en muchas
bibliotecas como jQuery y Lodash. También permite que tu código sea
expresivo y menos verboso. Por eso, digo, utiliza el encadenamiento de
métodos y échale un vistazo a lo limpio que llega a ser tu código. En tus
funciones de clases, simplemente devuelve el this al final de cada función
y asi puedes seguir encadenando los metodos de tu clase.
Prefiere composición en vez de la herencia
Como se ha dicho antes famosamente en el libro de Design Patterns escrito
por el Gang of Four, debes preferir composición en vez de la herencia
cuando puedas. Hay muchas razones para utilizar estos dos modelos. El
punto importante aquí es que tu mente naturalmente quiere utilizar la
herencia, así que intenta pensar si composición también podría resolver tu
problema. En algunos casos, puede que sea la solución.
Puede que te preguntes, ¿”cuando debería de utilizar la herencia?" Bueno,
depende de tu problema del momento, pero esta sería una lista decente de
cuando tiene más sentido utilizarla que la composición
1. Tu herencia representa una relación de "es-un" y no un "tener-un"
(Humano->Animal vs Usuario->Detalles del Usuario)
2. Puedes reutilizar tu código de las clases bases (Los humanos pueden
moverse como todos los animales)
3. Quieres hacer cambios globales a las clases derivadas con cambiar
una clase base. (Cambiar el gasto calórico de todos los animales
cuando se mueven)
SOLID
El principio único de responsabilidad (SRP)
Como se menciona en Clean Code, "Nunca debe existir más que una sola
razón para cambiar una clase". Vale la pena decir que es normal querer
llenar una 'clase' con muchas funciones, igual que cuando solo te permiten
llevar una maleta en el vuelo. El problema existe en que tu 'clase' no estará
cohesiva conceptualmente y existirá muchas razones para cambiarse.
Minimizar la cantidad de veces que necesitas cambiar una clase es
importante. Es importante ya que con demasiada funcionalidad viene
dificultad de modificarlo y entender cómo afecta a otros módulos
dependientes en tu programa.
Principio de abierto/cerrado (OCP)
Como dijo Bertrand Meyer, "las entidades de software (clases, módulos,
funciones, etc.) deben abrirse para extensión, pero cerrarse para
modificación. ¿Qué significa eso? Bueno, este principio básicamente nos
dice que debes permitir que tus usuarios introduzcan nuevas
funcionalidades sin cambiar el código existente
El principio de sustitución de Liskov (LSP)
Este es un término espantoso para un concepto muy simple. Formalmente
se define como "Si S es un subtipo de T que los objetos de T se reemplazan
con los objetos de tipo S". (Es decir, los objetos de tipo S se pueden
substituir con los objetos de tipo T sin alterar las propiedades deseables del
programa (precisión, actuación, etc.). Esa si es una definición aún más
espantosa.
La mejor explanación para este concepto es si tienes una clase padre y una
clase hijo, luego la clase base y la clase hijo se pueden intercambiar sin
tener resultados que carecen de precisión. Puede que aun estas confundido,
así que miremos al modelo clásico de rectángulo-cuadro. Matemáticamente,
un cuadro es un rectángulo, pero si lo modelas como una relación de "es-
un" con la herencia, te meterás en problemas rápidamente.
El principio de segregación en cuanto a los interfaces (ISP)
JavaScript no tiene interfaces así que este principio no se aplica tanto como
en otros idiomas. Sin embargo, es importante y es relevante, aunque
JavaScript no tenga un sistema de tipos.
ISP declara que "Los clientes no se deben forzar para depender en
interfaces que no implementan". Los interfaces son contratos implícitos en
JavaScript debido al teclear de duck.
Un buen ejemplo que demuestra este principio en JavaScript es para las
clases que necesitan objetos grandes de composición. Con no requerir que
los clientes se encarguen de muchas opciones, puedes beneficiar ya que la
mayoría del tiempo no hace falta todo lo extra. Cuando haces que las
opciones del contratos sean opcionales, evitas un "interfaz gordo".
El principio de la inversión de las dependencias (DIP)
Este principio declara dos cosas esenciales:
1. Los módulos de nivel alto no deben depender en los módulos de nivel bajo.
Los dos deben dependerse en las abstracciones.
2. Las abstracciones no deben dependerse en las detalles. Las detalles deben
dependerse en las abstracciones.
Esto ha de entender la primera vez, pero si has trabajado con AngularJS, has
visto una implementación de este principio en la forma de la inyección de
dependencias (DI). Mientras que no son conceptos idénticos, el DIP
mantiene que los módulos de nivel alto no sepan las detalles de los módulos
de nivel bajo y también se encarga de ellos. Esto se puede conseguir con DI.
Un beneficio enorme de esto es que reduce la convivencia entre los
módulos. La convivencia es un modelo muy malo en cuanto al desarrollo de
software ya dificulta la posibilidad de refactorizar.
Como se ha mencionado previamente, JavaScript no tiene interfaces así que
las abstracciones de las que se dependen son contratos implícitos. Es decir,
los métodos y las propiedades que un objeto/clase expone hasta otro
objeto/clase. En el ejemplo más abajo, el contrato implícito es que cualquier
módulo de Request que utilizar el InventoryTracker debe tener un método
de requestItems.