Como desarrolladores, hemos pasado mucho tiempo picando el código con la única preocupación de que funcione, sin preocuparnos demasiado en la forma del código, arquitectura, escalabilidad, mantenibilidad o legibilidad.
Todos hemos tenido plazos de entrega apretados, pero las prisas, cuando se dan repetitivamente derivan en un código cada vez más difícil de mantener.
No nos debemos conformar con hacer las cosas para que funcionen, nos debemos preocupar por obtener un código que todos los desarrolladores podamos entender.
Lo que permitirá que distintos equipos que no se conocen, puedan trabajar con un mismo producto, no es el lenguaje de programación, sino las normas, nomenclaturas, principios y patrones de diseño que se aplican en el proyecto.
Durante este artículo no hablaremos de principios SOLID, ni patrones de diseño, sin duda un nivel más avanzado en el mundo del Clean Code.
Nos centraremos en pequeñas normas que, si tenemos en cuenta durante nuestro desarrollo, nos permitirá obtener un código más legible que permita a otros equipos mantener ese código sin perder horas, o incluso días, intentando entender una función. Incluso a nosotros mismos nos resultará más fácil retomar nuestro propio código semanas después.
El mal código tiene un coste y cuesta tanto hacer las cosas bien, como hacerlas mal. Sólo necesitamos saber cómo hacerlas mejor.
A continuación, os explicamos seis puntos básicos que os ayudarán a mejorar vuestro código y el resultado final de vuestro proyecto.
El ‘Switch’ no es un mal patrón en sí, es un condicional que normalmente se usa para evitar largos una cadena de ‘if-else-if-…’. Pero, aun así, tienen una sintaxis muy redundante y pueden derivar también en métodos muy largos debido a ‘case’ demasiado grandes. Dificultando la legibilidad y el debugging al distribuir la lógica en cada ‘case’.
Existen distintas formas de evitar el uso de ‘Switch’ que derivan el código más legible y escalable, y evita la redundancia de un ‘Switch’ con la misma condición.
Personalmente, la forma que yo prefiero es usar poliformismo, de forma que la propiedad que estamos añadiendo al condicional se convierte en un objeto que implementa la lógica que debería ir en cada ‘case’.
Veamos un ejemplo a continuación:
for (var animal in home) {
switch (typeof(animal)) {
case "dog":
animal.bark();
break;
case "cat":
animal.meow();
break;
}
}
for(IAnimal animal in animals) {
animal.speak();
}
interface IAnimal{
Speak();
}
class Dog extends IAnimal
{
Speak(){
return 'Guau'
}
}
class Cat extends IAnimal
{
Speak(){
return 'Miau'
}
}
Qué el código sea descriptivo es un ‘must’, y es fácil pensar en nombrar las cosas adecuadamente, aunque también es muy complicado encontrar el nombre adecuado.
No obstante, sólo tener presente que tenemos que describir mejor las cosas hará nuestro código más entendible y fácil de leer.
En el caso de los condicionales, es mucho más agradecido e inmediato leer y entender un condicional cuya condición es un método del tipo:
/*Aproximación básica*/
if (isActiveStudent(user))
/*Usando una aproximación más propia de la orientación a objetos*/
if(user?.isActiveStudent())
Que leer algo como:
if (user != null && user.RoleId==3 && (user.FinishDate !== null || user.FinishDate > Today))
Sólo con sacar la condición a un método descriptivo, la haremos entendible al momento.
En el código, tanto las clases, como funciones y variables deben tener un nombre que refleje su intención.
Por lo que, nunca se deben usar acrónimos o variables carácter, como, por ejemplo:
f.i var u = User()
Ya que sólo la persona que las ha creado las entiende, y al cabo de un tiempo, seguramente se olvide también de los acrónimos utilizados.
Las funciones que hacen más de una cosa son de las peores cosas que podemos hacer picando código. Dichas funciones, acoplan código de forma que sólo se puede llamar ese método si necesitamos hacer las “X acciones” que hace ese método.
De esta manera, nos vemos obligados a duplicar ese código desde otro lugar, desde el que necesitamos ejecutar cierta lógica.
Por lo que estas funciones, dificultan entender lo que hace ese código y terminan siendo métodos con excesivas líneas de código, cuando debemos mantener los métodos lo más cortos posibles. Siempre es mejor partir un método, aunque solo sea por legibilidad, que dejar un método.
Si, lo sé, si nos volvemos extremistas con este principio, nunca podríamos definir un método que llamará a otros dos métodos en su implementación. Ya que este método padre estaría haciendo dos cosas.
Tranquilos/las, no hay que volverse loco. Lo importante a tener en cuenta, es que el método padre, en sí, no hace nada, lo hacen todo los dos métodos hijos, con lo que no estaríamos contradiciendo nada.
De todos modos, la idea importante aquí es intentar mantener los métodos lo más cortos posibles, y encapsular debidamente la lógica para que cada método haga lo que deba y no más. Esto mejorará increíblemente la legibilidad y reusabilidad del código.
Repetir una línea de código es algo muy grave, deriva en situaciones en las que toca reparar algo en tantos sitios como esa lógica se haya duplicado y cuando toca evolucionar el código debes tener presente todos los puntos donde esa lógica se ha duplicado para aplicar ese cambio, derivando todo es una peor mantenibilidad el código.
Es lógico pensar que cuantas más líneas de código tiene el proyecto más complicada es de mantener.
Pero, ¿cómo evitamos duplicar código?
No hay una respuesta fácil, pero un primer paso es extraer la lógica a funciones debidamente parametrizadas, o extraer valores en variables.
Veamos un ejemplo a continuación:
function getAquaticAnimals() {
const allAnimals = getAllAnimals()
const aquaticAnimals = []
for (var animal in allAnimals)
if (animal.type === "aquatic") {
aquaticAnimals.push(animal)
}
}
return aquaticAnimals
}
function getLandAnimal() {
const allAnimals = getAllAnimals()
const landAnimal = []
for (var animal in allAnimals)
if (animal.type === "land") {
landAnimal.push(animal)
}
}
return landAnimal
}
function getFlyingAnimals() {
const allAnimals = getAllAnimals()
const flyingAnimals = []
for (var animal in allAnimals)
if (animal.type === "flying") {
flyingAnimals.push(animal)
}
}
return flyingAnimals
}
En el código anterior, podemos ver que las dos funciones están haciendo lo mismo, con la única diferencia del tipo de noticia. Así pues, si extraemos esa lógica en un método, podemos reusar ese código además de mejorar la legibilidad de este:
function getAquaticAnimals() {
const allAnimals = getAllAnimals()
return getAnimalNames(allAnimals, 'aquatic')
}
function getRustNews() {
const allAnimals = getAllAnimals()
return getAnimalNames(allAnimals, 'land')
}
function getFlyingAnimals() {
const allAnimals = getAllAnimals()
return getAnimalNames(allAnimals, 'flying')
}
function getAnimalNames(allAnimals, animalType) {
const animalNames = []
for (var animal in allAnimals) {
if (animal.type === animalType) {
animalNames.push(animal.name)
}
}
return animalNames
}
Habrás notado que la llamada “getAllAnimals()” se repite en cada método, esto podría ser una mala práctica o no, según el uso que le queramos dar a la función reutilizada ‘getAnimalNames’. Puede ser necesario parametrizar el conjunto de animales con el que trabaja la función.
function getAquaticAnimals() {
return getAllAnimalsNameBy('aquatic')
}
function getRustNews() {
return getAllAnimalsNameBy('land')
}
function getFlyingAnimals() {
return getAllAnimalsNameBy('flying')
}
function getAllAnimalsNameBy(animalType) {
const allAnimals = getAllAnimals()
const animalNames = []
for (var animal in allAnimals) {
if (animal.type === animalType) {
animalNames.push(animal.name)
}
}
return animalNames
}
Pero, si por otro lado, esa función siempre trabajará sobre el conjunto total de animales, es buena opción eliminar el parámetro ‘allAnimals’ y dejar los métodos de la siguiente forma.
Derivando en un código más legible aunque ligeramente menos reusable, aunque siempre podemos crear una segunda función ‘getAnimalNames(allAnimals, type)’ que nos devuelve la reusabilidad.
function getAllAnimalsNameBy(animalType) {
const allAnimals = getAllAnimals()
return getAnimalNames(allAnimals,animalType)
}
function getAnimalNames(animalsArray, animalType) {
const animalNames = []
for (var animal in animalsArray) {
if (animal.type === animalType) {
animalNames.push(animal.name)
}
}
return animalNames
}
Como antiguo y orgulloso Boy Scout, siempre solíamos decir algo como ‘Hay que dejar el bosque más limpio de lo que lo hemos encontrado”. Así que, al irnos de un lugar, recogíamos lo nuestro, y siempre nos llevamos algo de basura que encontrábamos a nuestro alrededor.
Esta es una norma que aplica perfectamente en el bosque que representa nuestro código.
Cada vez que vayamos de visita al código, debemos hacer las cosas bien, y aplicar entre otros los principios que hemos descrito anteriormente, para así no añadir más basura.
Siempre hay que dejar el entorno un poco mejor de lo que lo hemos encontrado, aunque nosotros no seamos responsables de ese código. Así, conseguimos que, a base de pequeñas iteraciones o visitas, poco a poco, iremos mejorando el código y literalmente dejándolo más limpio.
Además, todo el tiempo que hemos dedicado a intentar entender el propósito de un método o función que nos hemos encontrado, lo podemos ahorrar al siguiente desarrollador si mejoramos de alguna forma esas partes nada claras y ofuscadas, en definitiva, sucias.
Nosotros los programadores somo los responsables de escribir código de calidad. El cliente debe notar que la calidad no está sólo en que se respete la funcionalidad sino que resolver bugs, o realizar evolutivos resulta fácil y sin sobrecoste por códigos mal implementados, ya que las malas decisiones técnicas las paga tanto el cliente como el desarrollador.
Existen muchas más normas que las que hemos comentado, pero con aplicar las mencionadas la legibilidad y mantenibilidad del código mejorará enormemente, y por consiguiente, la productividad.
Más allá de nuevos Frameworks, lenguajes de programación o servicios, es nuestra responsabilidad como desarrolladores aprender técnicas de programación agnósticas para volvernos mejores programadores y poder ofrecer productos de calidad, tanto por fuera, como por dentro.