JavaScript

Principios SOLID en JavaScript: Guía para un Código de Calidad Profesional

Felipe 7 min de lectura
Domina los principios SOLID en JavaScript Vanilla para crear aplicaciones más mantenibles, escalables y fáciles de testear con ejemplos prácticos y sencillos.

¿Alguna vez has sentido que tocar una sola línea de tu código en JavaScript rompe tres cosas diferentes en partes del proyecto que ni recordabas que existían? Si la respuesta es sí, bienvenido al club del “Código Espagueti”. Pero no te preocupes, hay una cura: los principios SOLID.

En este artículo, vamos a desglosar qué son estos principios y cómo aplicarlos en JavaScript Vanilla para que tu código pase de ser un rompecabezas imposible a una obra maestra de ingeniería.

¿Qué son los principios SOLID? (Respuesta rápida)

SOLID es un acrónimo que representa cinco reglas de diseño de software destinadas a hacer que los sistemas sean más mantenibles, flexibles y fáciles de entender.

LetraPrincipioEn pocas palabras…
SSingle ResponsibilityUna clase o función debe hacer solo una cosa.
OOpen/ClosedAbierto para extender, cerrado para modificar.
LLiskov SubstitutionLas subclases deben poder sustituir a sus clases base.
IInterface SegregationNo obligues a nadie a depender de lo que no usa.
DDependency InversionDepende de abstracciones, no de implementaciones.

1. S: Responsabilidad Única (SRP)

El principio de Responsabilidad Única nos dice que una clase, módulo o función debe tener un solo motivo para cambiar. Si tu función saveUser también valida el email, envía un correo de bienvenida y actualiza el DOM… tienes un problema.

El error común:

function handleUser(user) {
  // Validación
  if (user.email.includes('@')) {
    // Guardar en DB
    db.save(user);
    // Actualizar UI
    document.getElementById('status').innerText = 'Usuario guardado';
  }
}

Aplicando SRP:

Dividimos las responsabilidades para que cada pieza sea independiente y reutilizable.

const validator = {
  isValidEmail: (email) => email.includes('@')
};

const userRepository = {
  save: (user) => db.save(user)
};

const uiManager = {
  showStatus: (message) => {
    document.getElementById('status').innerText = message;
  }
};

// La lógica principal solo orquesta
function createUser(user) {
  if (validator.isValidEmail(user.email)) {
    userRepository.save(user);
    uiManager.showStatus('Usuario guardado con éxito');
  }
}

2. O: Abierto/Cerrado (OCP)

Este principio dicta que las entidades de software deben estar abiertas para la extensión, pero cerradas para la modificación. Es decir, deberías poder añadir nuevas funcionalidades sin tocar el código que ya funciona.

El problema:

Imagina que tienes un sistema de descuentos y cada vez que añades un nuevo tipo de cliente, tienes que modificar un switch.

function getDiscount(userType) {
  if (userType === 'VIP') return 0.2;
  if (userType === 'PREMIUM') return 0.1;
  return 0; // Si mañana hay 'GOLD', hay que modificar esta función
}

Aplicando OCP:

Usamos polimorfismo o un mapa de estrategias.

const discounts = {
  VIP: 0.2,
  PREMIUM: 0.1,
  GOLD: 0.3, // ¡Añadir uno nuevo es así de fácil!
  DEFAULT: 0
};

function getDiscount(userType) {
  return discounts[userType] || discounts.DEFAULT;
}

3. L: Sustitución de Liskov (LSP)

Si tienes una clase base y una subclase, deberías poder intercambiarlas sin que tu aplicación explote o se comporte de forma errática. El error más común en la vida real es crear subclases que “anulan” o lanzan errores en métodos que la clase padre sí debería soportar.

El problema real (Procesadores de Pago):

Imagina que tienes un sistema de pagos. Todos los pagos deberían poder “procesarse”, ¿verdad? Pero, ¿qué pasa cuando añades un método de pago que requiere una validación manual (como una transferencia bancaria)?

class PaymentProcessor {
  process(amount) {
    console.log(`Procesando pago de $${amount}...`);
    return true;
  }
}

class CreditCardProcessor extends PaymentProcessor {
  process(amount) {
    // Lógica automática de tarjeta
    return super.process(amount);
  }
}

class BankTransferProcessor extends PaymentProcessor {
  process(amount) {
    // ¡PROBLEMA! La transferencia no es inmediata, requiere un recibo.
    // Si el resto de tu código espera que esto sea automático, fallará.
    throw new Error("Las transferencias requieren validación manual.");
  }
}

Aplicando LSP (La solución práctica):

No asumas que todos los pagos se procesan igual. Divide tus clases por comportamiento real para que el código que las usa sepa exactamente qué esperar.

class Payment {
  constructor(amount) {
    this.amount = amount;
  }
}

// Pagos que se procesan instantáneamente
class AutomaticPayment extends Payment {
  execute() {
    console.log(`Pago de $${this.amount} ejecutado con éxito.`);
  }
}

// Pagos que requieren pasos extra
class ManualPayment extends Payment {
  generateInstructions() {
    console.log(`Instrucciones para pagar $${this.amount} enviadas al email.`);
  }
}

// Ahora el código que usa estas clases no se lleva sorpresas:
const processAnyPayment = (payment) => {
  if (payment instanceof AutomaticPayment) {
    payment.execute();
  } else if (payment instanceof ManualPayment) {
    payment.generateInstructions();
  }
};

4. I: Segregación de Interfaces (ISP)

En JavaScript no tenemos la palabra clave interface, pero el concepto es simple: No obligues a nadie a depender de cosas que no usa. En lugar de crear una “Clase Navaja Suiza” que lo hace todo, crea piezas pequeñas y específicas.

El error común (La Clase “Gorda”):

Imagina que tienes una clase para manejar dispositivos de oficina.

class SmartPrinter {
  print() { console.log("Imprimiendo..."); }
  scan() { console.log("Escaneando..."); }
  fax() { console.log("Enviando Fax..."); }
}

// Si tienes una impresora básica que SOLO imprime...
class BasicPrinter extends SmartPrinter {
  scan() {
    // ¡PROBLEMA! Estamos obligados a tener este método aunque no sirva.
    throw new Error("No tengo escáner");
  }
}

Aplicando ISP (Divide y vencerás):

La solución es separar las responsabilidades en clases o objetos pequeños. Así, cada dispositivo solo tiene lo que realmente necesita.

// Definimos piezas pequeñas y específicas
class Printer {
  print() { console.log("Imprimiendo documento..."); }
}

class Scanner {
  scan() { console.log("Escaneando documento..."); }
}

// 1. Una impresora sencilla solo usa la pieza de imprimir
class BasicPrinter extends Printer {}

// 2. Una máquina moderna usa ambas piezas (Composición simple)
class AllInOne {
  constructor() {
    this.printer = new Printer();
    this.scanner = new Scanner();
  }
}

const mySimplePrinter = new BasicPrinter();
mySimplePrinter.print(); // Solo tiene lo que necesita.

5. D: Inversión de Dependencias (DIP)

Los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deben depender de abstracciones. En términos simples: inyecta tus dependencias.

El problema (Acoplamiento fuerte):

class PasswordReminder {
  constructor() {
    this.db = new MySQLConnection(); // Acoplado a MySQL para siempre
  }
}

Aplicando DIP:

Pasamos la conexión como argumento. Ahora el Reminder no sabe ni le importa qué base de datos usas.

class PasswordReminder {
  constructor(databaseConnection) {
    this.db = databaseConnection; // Puede ser MySQL, MongoDB, LocalStorage...
  }
}

const reminder = new PasswordReminder(new PostgreSQLConnection());

Tu código te lo agradecerá mañana

Aplicar SOLID no significa que debas sobre-complicar cada pequeña función de tu proyecto. Se trata de tener una mentalidad de escalabilidad.

La próxima vez que escribas una clase o un componente, pregúntate:

  • ¿Está haciendo demasiadas cosas? (S)
  • ¿Puedo añadir una opción nueva sin romper lo anterior? (O)
  • ¿Estoy forzando herencias raras? (L)
  • ¿Pido datos que no uso? (I)
  • ¿Puedo cambiar mis herramientas externas sin reescribir todo? (D)

Escribir código limpio es una inversión que pagará dividendos en forma de menos bugs y más tiempo libre para disfrutar de un buen café (o lo que prefieras). ¡A programar con cabeza!

#solid #javascript #clean code #arquitectura de software #patrones de diseño #programación orientada a objetos

Artículos relacionados