Original article: How to Write Clean Code – Tips and Best Practices (Full Handbook)

¡Hola a todos! En este manual vamos a hablar sobre cómo escribir código "limpio". Es un tema que solía confundirme un poco cuando comenzaba como programador, y encuentro que tiene muchas matices e interpretaciones posibles.

Por lo tanto, en este artículo vamos a hablar sobre lo que el término "código limpio" significa, por qué es importante, cómo podemos evaluar si el código base está limpia o no. También vas a aprender algunas mejores prácticas y convenciones que puedes seguir para hacer que tu código sea más limpio.

¡Vamos!

Tabla de contenido

¿Qué significa escribir "código limpio" y por qué debería importarme?

Código limpio es un término usado para describir código de computadoras que es fácil de leer, entender y mantener. Código limpio se escribe de una manera que lo hace simple, conciso y expresivo. Sigue un conjunto de convenciones, estándares y prácticas que lo hacen fácil de leer y seguir.

El código limpio es libre de complejidad, redundancias y otros hedores de código ("code smells") y  anti-patrones que pueden hacerlo difícil de mantener, depurar y modificar.

No puedo exagerar la importancia del código limpio. Cuando el código es fácil de leer y entender, es más fácil para los desarrolladores trabajar en el código base. Esto puede llevar a incrementar la productividad y una reducción de errores.

También, cuando el código es fácil de mantener, se asegura que el código base puede ser mejorado y actualizado con el tiempo. Esto es especialmente importante para proyectos a largo plazo donde el código tiene que ser mantenido y actualizado por años venideros.

¿Cómo puedo evaluar si una base de código está limpia o no?

Puedes evaluar el código limpio en una variedad de maneras. Buena documentación, formato consistente y una base de código bien organizada, son indicadores de código limpio.

Las revisiones de código también pueden ayudar a identificar posibles problemas y asegurar que el código siga las mejores prácticas y convenciones.

El testing también es un aspecto importante del código limpio. Ayuda a asegurarnos que el código está funcionando como es esperado y puede detectar errores temprano.

Existen varias herramientas, prácticas y convenciones que puedes implementar para asegurar una base de código limpia. Al implementar estas herramientas y prácticas, los desarrolladores pueden crear una base de código que sea fácil de leer, entender y mantener.

También es importante recordar que hay una cantidad inevitable de subjetividad relacionada con este tema, y que hay distintas opiniones y consejos allí afuera. Lo que puede parecer limpio y asombroso para una persona o un proyecto, podría no ser así para otra persona u otro proyecto.

Sin embargo, existen algunas convenciones generales que podemos seguir para lograr código más limpio, así que vamos a eso ahora.

consejos y convenciones para escribir código limpio

Eficacia, Eficiencia y Simplicidad

Siempre que necesito pensar sobre cómo implementar una nueva característica a la base de código ya existente, o cómo abordar la solución de un problema específico, siempre priorizo estas tres simples cosas.

Eficacia

Primero, nuestro código tiene que ser eficaz, lo que significa que debería resolver el problema que se supone que debe resolver. Por supuesto que esta es la expectativa más básica que podemos tener para nuestro código, pero si nuestra implementación en realidad no funciona, es inútil pensar en otra cosa.

Eficiencia

Segundo, una vez que sabemos que nuestro código resuelve el problema, deberíamos verificar si lo hace eficientemente. ¿El programa corre utilizando una cantidad razonable de recursos en términos de tiempo y espacio? ¿Puede correr más rápido con menos espacio?

La complejidad algorítmica es algo que deberías estar al tanto para poder evaluar esto.

Para ampliar la eficiencia, aquí hay dos ejemplos de una función que calcula la suma de todos los números en un arreglo.

// Ejemplo ineficiente
function sumArrayInefficient(array) {
  let sum = 0;
  for (let i = 0; i < array.length; i++) {
    sum += array[i];
  }
  return sum;
}

Esta implementación de la función sumArrayInefficient itera sobre el arreglo usando un bucle for y suma cada elemento a la variable sum. Es una solución válida, pero no es muy eficiente porque requiere iterar sobre el arreglo completo, sin importar su tamaño.

// Ejemplo eficiente
function sumArrayEfficient(array) {
  return array.reduce((a, b) => a + b, 0);
}

Esta implementación de la función sumArrayEfficient utiliza el método reduce para sumar los elementos del arreglo. El método reduce aplica una función a cada elemento del arreglo y acumula el resultado. En este caso, la función simplemente suma cada elemento al acumulador, el cual comienza en 0.

Esta es una solución más eficiente, ya que sólo requiere una única iteración sobre el arreglo y realiza la operación sumatoria sobre cada elemento a medida que avanza.

Simplicidad

Y por último viene la simplicidad. Esto es lo más difícil de evaluar porque es subjetivo, depende de la persona que lea el código. Pero algunas pautas que podemos seguir son:

  1. ¿Puedes entender fácilmente lo que hace el programa en cada línea?
  2. ¿Las funciones y variables tienen nombres que representan claramente sus responsabilidades?
  3. ¿El código tiene sangrado y espaciado correcto con el mismo formato a lo largo de la base de código?
  4. ¿Hay documentación disponible para el código? Se usan comentarios para explicar partes complejas del programa?
  5. ¿Qué tan rápido puedes identificar en qué parte de la base de código están ciertas características del programa? ¿Puedes eliminar/agregar nuevas características sin la necesidad de modificar varias otras partes del código?
  6. ¿El código sigue un enfoque modular, con diferentes características separadas en componentes?
  7. ¿Se reutiliza el código cuando es posible?
  8. ¿Se siguen las mismas decisiones de arquitectura, diseño e implementación, por igual a lo largo de la base de código?

Al seguir y priorizar estos tres conceptos de eficacia, eficiencia y simplicidad, siempre vamos a tener pautas para seguir cuando estemos pensando sobre cómo implementar una solución. Ahora extendamos sobre algunas de las pautas que pueden ayudarnos a simplificar nuestro código.

Formato y Sintaxis

Usar formato y sintaxis consistente a lo largo de la base de código es un aspecto importante de escribir código limpio. Esto es porque el formato y sintaxis consistente hace que el código sea más legible y fácil de entender.

Cuando el código es consistente, los desarrolladores pueden identificar fácilmente patrones y entender cómo funciona el código, lo que hace más fácil la depuración, mantenimiento y actualización de la base de código con el tiempo. La consistencia también ayuda a reducir errores, ya que asegura que todos los desarrolladores estén siguiendo las mismas normas y convenciones.

Algunas de las cosas que debemos pensar en relación con el formato y sintaxis son:

  • Espaciado y sangrado
// mal espaciado y sangrado
const myFunc=(number1,number2)=>{
const result=number1+number2;
return result;
}

// buen espaciado y sangría
const myFunc = (number1, number2) => {
    const result = number1 + number2
    return result
}

Aquí tenemos un ejemplo de una misma función, una hecha sin sangría ni espaciado, y la otra debidamente espaciada y sangrados. Podemos ver que la segunda es claramente más fácil de leer.

  • Sintaxis consistente
// Función de flecha, sin punto y coma, sin no return
const multiplyByTwo = number => number * 2

// Función, punto y comas, return
function multiplyByThree(number) {
    return number * 3;
}

De nuevo, aquí tenemos implementadas funciones muy parecidas con sintaxis diferente. La primera es una función de flecha, sin punto y coma, y sin return, mientras que la otra es una función común que usa punto y coma y un return.

Ambas funcionan y están muy bien, pero deberíamos apuntar a siempre usar el mismo sintaxis para operaciones similares, ya que se vuelve más uniforme y legible a lo largo de la base de código.

Los linters y formateadores de código son excelentes herramientas que podemos usar en nuestros proyectos para automatizar las convenciones de sintaxis y formatos en nuestra base de código.

Convenciones de Mayúsculas y Minúsculas consistentes

// camelCase
const myName = 'John'
// PascalCase
const MyName = 'John'
// snake_case
const my_name = 'John'

Lo mismo va para la convención de mayúsculas y minúsculas que elijamos seguir.  Todas éstas funcionan, pero tenemos que apuntar a usar de manera consistente la misma a lo largo de nuestro proyecto.

Denominación

Nombrar variables y funciones de manera clara y descriptiva es un aspecto importante de escribir código limpio. Esto ayuda a mejorar la legibilidad y mantenibilidad de la base de código. Cuando los nombres están bien elegidos, otros desarrolladores pueden entender rápidamente qué está haciendo la variable o función, y cómo está relacionada con el resto del código.

Aquí hay dos ejemplos en JavaScript que demuestran la importancia de tener nombres claros y descriptivos:

// Ejemplo 1: Denominación pobre
function ab(a, b) {
  let x = 10;
  let y = a + b + x;
  console.log(y);
}

ab(5, 3);

En este ejemplo tenemos una función que toma dos parámetros, los suma a un valor predefinido de 10, y muestra por consola el resultado. El nombre de la función y nombres de variables están mal elegidos y no dan ninguna indicación de lo que la función hace o lo que las variables representan.

// Ejemplo 1: Buena denominación
function calcularTotalConImpuesto(precioBase, tasaImpuesto) {
  const IMPUESTO_BASE = 10;
  const totalConImpuesto = precioBase + (precioBase * (tasaImpuesto / 100)) + IMPUESTO_BASE;
  console.log(totalConImpuesto);
}

calcularTotalConImpuesto(50, 20);

En este ejemplo tenemos una función que calcula el precio total de un producto incluyendo el impuesto. El nombre de la función y nombres de variables están bien elegidos y dan una clara indicación de lo que la función hace y lo que las variables representan.

Esto hace que el código sea más fácil de leer y entender, especialmente para otros desarrolladores que pueden estar trabajando con la base de código en el futuro.

Concisión vs Claridad

Al momento de escribir código limpio es importante encontrar un balance entre concisión y claridad. Si bien es importante mantener el código conciso para mejorar la legibilidad y mantenibilidad, es igual de importante asegurar que el código sea claro y fácil de entender. Escribir código demasiado conciso puede llegar a confusión y errores, y puede hacer que el código sea difícil de trabajar para otros desarrolladores.

Aquí hay dos ejemplos que demuestran la importancia de concisión y claridad:

// Ejemplo 1: Función concisa
const cuentaVocales = s => (s.match(/[aeiou]/gi) || []).length;
console.log(cuentaVocales("hola mundo"));

Este ejemplo usa la función flecha concisa y expresiones regulares para contar el número de vocales en una cadena de caracteres dada. Si bien el código es bastante corto y fácil de escribir, puede que no sea inmediatamente claro para otro desarrollador saber cómo funciona el patrón de expresión regular, especialmente si no están familiarizados con la sintaxis de expresiones regulares.

// Ejemplo 2: Función más detallada y más clara
function contarVocales(s) {
  const vocalRegex = /[aeiou]/gi;
  const coincidencias = s.match(vowelRegex) || [];
  return coincidencias.length;
}

console.log(contarVocales("hola mundo"));

Este ejemplo usa una función tradicional y expresión regular para contar el número de vocales en una cadena dada, pero lo hace de una forma que es clara y fácil de entender. El nombre de la función y los nombres de variables son descriptivos, y al patrón de expresión regular se almacena con un nombre claro. Esto hace que se vea fácilmente lo que está haciendo la función y cómo funciona.

Es importante encontrar un balance entre concisión y claridad al escribir código. Si bien el código conciso puede mejorar la legibilidad y mantenibilidad, es importante asegurar que el código siga estándo claro y fácil de entender para otros desarrolladores que puedan trabjar en la base de código en el futuro.

Al usar nombres de funciones y variables descriptivos, y hacer uso de formateo de código claro y legible, y comentarios, es posible escribir código limpio y conciso que sea fácil de entender y trabajar con él.

Reusabilidad

La reusabilidad del código es un concepto fundamental en la ingeniería de software que hace referencia a la capacidad del código a user usado varias veces sin modificación.

La importancia de la reusabilidad del código se encuentra en el hecho que puede mejorar en gran medida la eficiencia y productividad del desarrollo del software al reducir la cantidad de código que necesita ser escrito y testeado.

Al reutilizar código existente, los desarrolladores pueden ahorra tiempo y esfuerzo, mejorar la calidad  y coherencia del código, minimizando el riesgo de introducir bugs y errores. El código reutilizable también permite arquitecturas de software más modulares y escalables, haciendo que sea más fácil mantener y actualizar la base de código con el tiempo.

// Ejemplo 1: Sin reusabilidad
function calcularAreaCirculo(radio) {
  const PI = 3.14;
  return PI * radio * radio;
}

function calcularAreaRectangulo(largo, ancho) {
  return largo * ancho;
}

function calcularAreaTriangulo(base, alto) {
  return (base * alto) / 2;
}

const areaCirculo = calcularAreaCirculo(5);
const areaRectangulo = calcularAreaRectangulo(4, 6);
const areaTriangulo = calcularAreaTriangulo(3, 7);

console.log(areaCirculo, areaRectangulo, areaTriangulo);

Este ejemplo define tres funciones que calculan el área de un círculo, rectángulo y triángulo, respectivamente. Cada función realiza una tarea específica, pero ninguna de ellas es re-utilizable para otras tareas similares.

Además, el uso de un valor PI predefinido puede llevar a errores si en el futuro el valor necesita cambiarse. El código es ineficiente debido a que repite varias veces la misma lógica.

// Ejemplo 2: Implementando reusabilidad
function calcularArea(forma, ...args) {
  if (forma === 'circulo') {
    const [radio] = args;
    const PI = 3.14;
    return PI * radio * radio;
  } else if (forma === 'rectangulo') {
    const [larho, ancho] = args;
    return larho * ancho;
  } else if (forma === 'triangulo') {
    const [base, altura] = args;
    return (base * altura) / 2;
  } else {
    throw new Error(`Forma "${forma}" no soportada.`);
  }
}

const areaCirculo = calcularArea('circulo', 5);
const areaRectangulo = calcularArea('rectangulo', 4, 6);
const areaTriangulo = calcularArea('triangulo', 3, 7);

console.log(areaCirculo, areaRectangulo, areaTriangulo);

Este ejemplo define una sola función calcularArea que toma el argumento forma y una cantidad variable de argumentos. En base al argumento forma la función realiza el cálculo apropiado y devuelve el resultado.

Este enfoque es mucho más eficiente ya que eliminamos la necesidad de repetir código para tareas similares. También es más flexible y extensible ya que se puede agregar fácilmente formas adicionales en el futuro.

Flujo Claro de Ejecución

Tener un flujo claro de ejecución es fundamental para escribir código limpio debido a que esto hace que el código sea más fácil de leer, entender y mantener.  El código que siga una estructura clara y lógica es menos propensa a errores, fácil de modificar y extender, y más eficiente en términos de tiempo y recursos.

Por otro lado, el código espagueti es un término usado para describir código que es complejo y difícil de seguir, a menudo caracterizado por bloques de código largos, enredado y desorganizado. El código espagueti puede ser el resultado de malas decisiones de diseño, acoplamiento excesivo, o falta de documentación y comentarios adecuados.

Aquí hay dos ejemplos de código JavaScript que realiza la misma tarea, una con un flujo de ejecución claro, y el otro con código espagueti:

// Ejemplo 1: Flujo claro de ejecución
function calcularDescuento(precio, porcentajeDescuento) {
  const montoDescuento = precio * (porcentajeDescuento / 100);
  const precioDescontado = precio - montoDescuento;
  return precioDescontado;
}

const precioOriginal = 100;
const porcentajeDescuento = 20;
const precioFinal = calcularDescuento(precioOriginal, porcentajeDescuento);

console.log(precioFinal);

// Ejemplo 2: Código espagueti
const precioOriginal = 100;
const porcentajeDescuento = 20;

let precioDescontado;
let montoDescuento;
if (precioOriginal && porcentajeDescuento) {
  montoDescuento = precioOriginal * (porcentajeDescuento / 100);
  precioDescontado = precioOriginal - montoDescuento;
}

if (precioDescontado) {
  console.log(precioDescontado);
}

Como podemos ver, el ejemplo 1 sigue una estructura clara y lógica, con una función que toma los parámetros necesarios y devuelve al resultado calculado. Por otra parte, el ejemplo 2 es mucho más complejo, con variables declaradas fuera de cualquier función y varias sentencias if usadas para verificar si el bloque de código se ha ejecutado con éxito.

Principio de Responsabilidad Única

El Principio de Responsabilidad Única (PRU) es un principio en el desarrollo de software que dice que cada clase o módulo debe tener sólo una razón para cambiar, o en otras palabras, cada entidad en nuestra base de código debe tener una única responsabilidad.

Este principio nos ayuda a crear código que es fácil de entender, mantener y extender.

Al aplicar PRU podemos crear código que es más fácil de testear, reusar y refactorizar, ya que cada módulo sólo maneja una única responsabilidad. Esto hace que sea menos probable que tenga efectos secundarios o dependencias que puedan dificultar el trabajo con el código.

// Ejemplo 1: Sin PRU
function procesarPedido(pedido) {
  // validar pedido
  if (pedido.items.length === 0) {
    console.log("Error: El Pedido no tiene elementos");
    return;
  }
  
  // calcular el total
  let total = 0;
  pedido.items.forEach(item => {
    total += item.precio * item.cantidad;
  });
  
  // aplicar descuentos
  if (pedido.cliente === "vip") {
    total *= 0.9;
  }
  
  // guardar pedido
  const db = new Database();
  db.connect();
  db.guardarPedido(pedido, total);
}

En este ejemplo, la función procesarPedido maneja varias responsabilidades: valida el pedido, calcula el total, aplica descuentos y guarda la órden en la base de datos. Esto hace que la función sea larga y difícil de entender, y cualquier cambio a una responsabilidad puede afectar a las otras, haciéndolo difícil de mantener.

// Ejemplo 2: Con PRU
class ProcesadorDePedidos {
  constructor(pedido) {
    this.pedido = pedido;
  }
  
  validar() {
    if (this.pedido.items.length === 0) {
      console.log("Error: El Pedido no tiene elementos");
      return false;
    }
    return true;
  }
  
  calcularTotal() {
    let total = 0;
    this.pedido.items.forEach(item => {
      total += item.precio * item.cantidad;
    });
    return total;
  }
  
  aplicarDescuentos(total) {
    if (this.pedido.cliente === "vip") {
      total *= 0.9;
    }
    return total;
  }
}

class GuardarPedido {
  constructor(pedido, total) {
    this.pedido = pedido;
    this.total = total;
  }
  
  guardar() {
    const db = new Database();
    db.connect();
    db.guardarPedido(this.pedido, this.total);
  }
}

const pedido = new Pedido();
const procesador = new ProcesadorDePedidos(order);

if (procesador.validar()) {
  const total = procesador.calcularTotal();
  const totalConDescuentos = procesador.aplicarDescuentos(total);
  const saver = new GuardarPedido(pedido, totalConDescuentos);
  saver.save();
}

En este ejemplo la función procesarPedido ha sido dividida en dos clases que siguen el PRU: ProcesadorDePedidos y GuardarPedido.

La clase ProcesadorDePedidos maneja las responsabilidades de validar el pedido, calcular el total y aplicar descuentos, mientras que la clase GuardarPedido maneja la responsabilidad de guardar el pedido en la base de datos.

Esto hace que el código sea más fácil de entender, hacer pruebas y mantener, ya que cada clase tiene una clara responsabilidad y puede ser modificada o reemplazada sin afectar a las demás.

Tener una "Fuente Única de la Verdad"

Tener una "fuente única de la verdad" significa que sólo existe un lugar donde se guarda un dato o configuración en particular  en la base de código, y cualquier otra referencia a él en el código se refiere a esa fuente. Esto es importante por que nos asegura que la información es consistente y evita la duplicación y contradicciones.

Aquí hay un ejemplo para ilustrar el concepto. Digamos que tenemos una aplicación que necesita mostrar las condiciones climáticas actuales en una ciudad. Podríamos implementar ésta característica de dos maneras diferentes:

// Opción 1: Sin "fuente única de la verdad"

// archivo 1: weatherAPI.js
const apiKey = '12345abcde';

function obtenerClimaActual(ciudad) {
  return fetch(`https://api.weather.com/conditions/v1/${city}?apiKey=${apiKey}`)
    .then(response => response.json());
}

// archivo 2: weatherComponent.js
const apiKey = '12345abcde';

function mostrarClimaActual(ciudad) {
  obtenerClimaActual(ciudad)
    .then(informacionClima => {
      // mostrar informacionClima en la UI
    });
}

En ésta opción, la clave API está duplicada en dos archivos distintos, esto hace difícil mantener y actualizar. Si alguna vez necesitamos cambiar la clave API, tenemos que recordar actualizarla en ambas partes.

// Opción 2: "Fuente Única de la Verdad"

// archivo 1: weatherAPI.js
const apiKey = '12345abcde';

function obtenerClimaActual(ciudad) {
  return fetch(`https://api.weather.com/conditions/v1/${city}?apiKey=${apiKey}`)
    .then(response => response.json());
}

export { obtenerClimaActual, apiKey };


// archivo 2: weatherComponent.js
import { obtenerClimaActual } from './weatherAPI';

function mostrarClimaActual(ciudad) {
  obtenerClimaActual(ciudad)
    .then(informacionClima => {
      // mostrar informacionClima en la UI
    });
}

En ésta opción se guarda la clave API en un lugar (en el archivo weatherAPI.js ) y exported para que la usen otros módulos. Esto asegura que solo exista una única fuente de la verdad para la clave API y evita la duplicación y contradicciones.

Si alguna vez necesitamos actualizar la clave API, lo podemos hacer en un lugar y todos los otros módulos que la utilizan automáticamente obtendrán el valor actualizado.

Expón y Consume los Datos que Necesitas

Un principio importante de escribir código limpio es solo exponer y consumir la información que sea necesaria para una tarea en particular. Esto ayuda a reducir la complejidad, aumenta la eficiencia y evita errores que pueden surgir de usar información innecesaria.

Cuando se expone o consume información que no es necesaria, puede llegar a problemas de desempeño y hacer que el código sea más difícil de entender y mantener.

Supongamos que tenemos un objeto con varias propiedades, pero solo necesitas usar un par de ellas. Una manera de hacer esto sería haciendo referencia al objeto y las propiedades específicas cada vez que las necesitemos. Pero esto puede volverse verboso y propenso a errores, especialmente si el objeto está profundamente anidado. Una solución clara y más eficiente sería usar la desestructuración de objeto para exponer y consumir únicamente la información que necesitas.

// Objeto original
const usuario = {
  id: 1,
  nombre: 'Alice',
  email: 'alice@example.com',
  edad: 25,
  direccion: {
    calle: '123 Main St',
    ciudad: 'Anytown',
    estado: 'CA',
    zip: '12345'
  }
};

// Expón y consume las propiedades nombre y email
const { nombre, email } = usuario;

console.log(nombre); // 'Alice'
console.log(email); // 'alice@example.com'

Modularización

La modularización es un concepto fundamental para escribir código limpio. Se refiere a la práctica de descomponer código largo y complejo en módulos o funciones pequeñas más manejables. Esto hace que el código sea fácil de entender, probar y mantener.

Utilizar la modularización nos otorga varios beneficios tales como:

  1. Re-usabilidad: Los módulos pueden ser reutilizados en distintas partes de la aplicación o en otras aplicaciones, ahorrando tiempo y esfuerzo en el desarrollo.
  2. Encapsulado: Los módulo te permiten ocultar los detalles internos de una función u objeto, exponiendo únicamente la interface básica al mundo exterior. Esto ayuda a reducir acoplamiento entre las distintas partes del código y mejorar la calidad del código en general.
  3. Escalabilidad: Al descomponer código largo en pequeñas partes modulares puedes fácilmente agregar o quitar funcionalidades sin afectar a toda la base de código.

Aquí hay un ejemplo en JavaScript de un fragmento de código que realiza una tarea sencilla, uno sin usar modularización y el otro implementando la modularización.

// Sin modularización
function calcularPrecio(cantidad, precio, impuesto) {
  let subtotal = cantidad * precio;
  let total = subtotal + (subtotal * impuesto);
  return total;
}

// Sin modularización
let cantidad = parseInt(prompt("Ingresar cantidad: "));
let precio = parseFloat(prompt("Ingresar precio: "));
let impuesto = parseFloat(prompt("Ingresar impuesto: "));

let total = calcularPrecio(cantidad, precio, impuesto);
console.log("Total: $" + total.toFixed(2));

En el ejemplo de arriba la función calcularPrecio es usada para calcular el precio total de un elemento dado la cantidad, el precio y el impuesto. Sin embargo, esta función no está modularizada y está estrechamente acoplado con la lógica de ingreso y salida del usuario. Esto puede hacerlo difícil de probar y mantener.

Ahora veamos un ejemplo del mismo código usando la modularización:

// Con modularización
function calcularSubtotal(cantidad, precio) {
  return cantidad * precio;
}

function calcularTotal(subtotal, impuesto) {
  return subtotal + (subtotal * impuesto);
}

// Con modularización
let cantidad = parseInt(prompt("Ingresar cantidad: "));
let precio = parseFloat(prompt("Ingresar precio: "));
let impuesto = parseFloat(prompt("Ingresar impuesto: "));

let subtotal = calcularSubtotal(cantidad, precio);
let total = calcularTotal(subtotal, impuesto);
console.log("Total: $" + total.toFixed(2));

En el ejemplo de arriba la función calcularPrecio ha sido descompuesta en dos funciones más pequeñas: calcularSubtotal y calcularTotal. Estas funciones ahora están modularizadas y son responsables de calcular el subtotal y total respectivamente. Esto hace que el código sea más fácil de entender, testear y mantener, y también lo hace más reutilizable en otras partes de la aplicación.

La modularización también puede hacer referencia a la práctica de dividir un archivo de código en varios archivos más pequeños que mas adelante son compilados de vuelta a un único archico (o menos archivos). Ésta práctica tiene los mismos beneficios que ya hemos hablado.

Estructura de Carpetas

Elegir una buena estructura de carpetas es una parte fundamental de escribir código limpio. Una estructura de proyecto bien organizada ayuda a los desarrolladores encontrar y modificar código fácilmente, reduce la complejidad del código y mejora la escalabilidad y mantenibilidad del proyecto.

Por otra parte, una mala estructura de carpetas puede hacer que sea un desafío entender la arquitectura del proyecto, navegar por la base de código y puede llevar a confusión y errores.

Aquí hay ejemplos de una buena y una mala estructura de carpetas utilizando un proyecto de React como ejemplo:

// Mala estructura de carpetas
my-app/
├── App.js
├── index.js
├── components/
│   ├── Button.js
│   ├── Card.js
│   └── Navbar.js
├── containers/
│   ├── Home.js
│   ├── Login.js
│   └── Profile.js
├── pages/
│   ├── Home.js
│   ├── Login.js
│   └── Profile.js
└── utilities/
    ├── api.js
    └── helpers.js

En éste ejemplo, la estructura del proyecto está organizada alrededor de tipos de archivos, tal como componentes, contenedores y páginas.

Pero este enfoque puede llevar a confusión y duplicación ya que no está claro qué archivos pertenecen a dónde. Por ejemplo, el componente Home está presente en ambas carpetas containers y pages. También puede ser un desafío encontrar y modificar código, debido a que los desarrolladores pueden necesitar navegar varias carpetas para encontrar el código que necesitan.

// Buena estructura de carpetas
my-app/
├── src/
│   ├── components/
│   │   ├── Button/
│   │   │   ├── Button.js
│   │   │   ├── Button.module.css
│   │   │   └── index.js
│   │   ├── Card/
│   │   │   ├── Card.js
│   │   │   ├── Card.module.css
│   │   │   └── index.js
│   │   └── Navbar/
│   │       ├── Navbar.js
│   │       ├── Navbar.module.css
│   │       └── index.js
│   ├── pages/
│   │   ├── Home/
│   │   │   ├── Home.js
│   │   │   ├── Home.module.css
│   │   │   └── index.js
│   │   ├── Login/
│   │   │   ├── Login.js
│   │   │   ├── Login.module.css
│   │   │   └── index.js
│   │   └── Profile/
│   │       ├── Profile.js
│   │       ├── Profile.module.css
│   │       └── index.js
│   ├── utils/
│   │   ├── api.js
│   │   └── helpers.js
│   ├── App.js
│   └── index.js
└── public/
    ├── index.html
    └── favicon.ico

En este ejemplo la estructura del proyecto está organizada alrededor de características, tales como componentes, páginas y utilidades. Cada característica tiene su propia carpeta, la cual contiene todos los archivos relacionados con esa característica.

Este enfoque hace que sea más fácil encontrar y modificar código ya que todos los archivos relacionados a una característica están ubicados en la misma carpeta. También reduce la duplicación y complejidad del código debido a que las características están separadas y sus archivos relacionado están organizados juntos.

Por lo general, una buena estructura de carpetas debería estar organizada alrededor de características, no los tipos de archivos, y tiene que facilitar la búsqueda y modificación de código. Una clara y lógica estructura puede hacer que el proyecto sea más fácil de mantener, entender y escalar, mientras que una estructura confusa e inconsistente puede llevar a errores y confusión.

Documentación

La documentación es una parte fundamental de escribir código limpio. Una documentación adecuada no sólo ayuda a los desarrolladores que escribieron el código a entenderlo mejor en el futuro pero también hace que sea más fácil para otros desarrolladores leer y entender la base de código. Cuando el código está bien documentado puede ahorrar tiempo y esfuerzo en depurar y mantener el código.

Documentar es especialmente importante en casos donde las soluciones simples y fáciles de entender no pueden ser implementadas, casos donde la lógica de negocio es bastante compleja, y casos en donde la gente que no está familiarizada con la base de código tiene que interactuar con ella.

Una manera de documentar código es usando comentarios. Los comentarios pueden dar contexto y explicar lo que el código está haciendo. Pero es importante usar los comentarios  sabiamente, solo comentar donde sea necesario y evitando los comentarios redundantes e innecesarios.

Otra forma de documentar código es usando documentación en línea. La documentación en línea está incorporada al código en sí y puede ser usada para explicar lo que hace una función or parte de código específica. La documentación en línea se usa a menudo en combinación con herramientas como JSDoc, que proporciona un estándar para documentar código en JavaScript.

Herramientas como Typescript también proporcionan documentación automática para tu base de código, lo cual es de mucha ayuda.

Por último, las herramientas como Swagger y Postman pueden ser usadas para documentar APIs, proporcionando una manera fácil de entender cómo interactuar con ellos.

Si te interesa saber cómo implementar, probar, consumir y documentar APIs, hace poco escribí dos guías para APIs REST y APIs GraphQL REST APIs and GraphQL APIs.

Terminando

Bueno, como siempre, espero que hayan disfrutado el artículo y hayan aprendido algo nuevo.

Si quieres, también puedes seguirme en LinkedIn o Twitter. ¡Nos vemos en la próxima!

giphy