DEV Community

Cover image for ¿Qué es SOLID? Principios de diseño para programar código limpio en TypeScript y React
John Serrano (DEV)
John Serrano (DEV)

Posted on • Originally published at johnserrano.co

¿Qué es SOLID? Principios de diseño para programar código limpio en TypeScript y React

Introducción a los principios SOLID en la Programación Orientada a Objetos

En el desarrollo de software, escribir código limpio, modular y fácil de mantener es fundamental. Aquí es donde entran los principios SOLID: un conjunto de reglas que ayudan a diseñar clases y componentes bien estructurados.

Pero, ¿qué es SOLID? ¿Es un patrón de diseño, una arquitectura, o algo más?

SOLID no es un patrón ni una arquitectura, sino cinco principios de diseño que nos ayudan a organizar mejor nuestro código, especialmente en la Programación Orientada a Objetos (POO).

En este artículo:

  • Explicaremos qué es la POO y por qué es importante para entender SOLID.
  • Veremos cada uno de los principios SOLID con ejemplos prácticos en TypeScript (backend) y React (frontend).
  • Analizaremos las ventajas de aplicar SOLID y cómo empezar a implementarlo.

1. Conceptos básicos de la POO

La Programación Orientada a Objetos (POO) es un paradigma que organiza el código en “objetos” que representan elementos del mundo real.

Cada objeto tiene atributos (características) y métodos (acciones).

Ejemplo sencillo:

// TypeScript (Backend)
class Usuario {
  nombre: string;
  correo: string;

  constructor(nombre: string, correo: string) {
    this.nombre = nombre;
    this.correo = correo;
  }

  iniciarSesion() {
    // lógica de autenticación
  }
}
Enter fullscreen mode Exit fullscreen mode
// React (Frontend)
function Usuario({ nombre, correo }) {
  return (
    <div>
      <h2>{nombre}</h2>
      <p>{correo}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Clases e instanciación

Las clases funcionan como plantillas para crear objetos. Por ejemplo, una clase SuperHeroe puede tener métodos como volar() y atributos como nombre.

Los 4 pilares de la POO

  • Abstracción: Resumir lo esencial de un objeto y ocultar los detalles innecesarios.
  • Encapsulamiento: Proteger los datos internos de un objeto, exponiendo solo lo necesario.
  • Herencia: Permitir que una clase hija herede atributos y métodos de una clase padre.
  • Polimorfismo: Permitir que diferentes clases respondan de manera distinta a un mismo método.

Abstracción

Extraer las características esenciales de un objeto y ocultar los detalles complejos.

// Solo exponemos lo necesario de la clase
abstract class Vehiculo {
  abstract moverse(): void;
}

class Auto extends Vehiculo {
  moverse() {
    console.log("El auto avanza sobre ruedas");
  }
}

const miAuto = new Auto();
miAuto.moverse(); // "El auto avanza sobre ruedas"
Enter fullscreen mode Exit fullscreen mode

Explicación:
La clase Vehiculo es una abstracción. No interesa cómo se mueve, solo que puede moverse. Cada subclase implementa el detalle.

Encapsulamiento

Proteger los datos internos de un objeto y exponer solo una interfaz controlada.

class CuentaBancaria {
  private saldo: number;

  constructor(saldoInicial: number) {
    this.saldo = saldoInicial;
  }

  depositar(monto: number) {
    this.saldo += monto;
  }

  obtenerSaldo(): number {
    return this.saldo;
  }
}

const cuenta = new CuentaBancaria(1000);
cuenta.depositar(500);
// cuenta.saldo = 99999; // Error: saldo es privado
console.log(cuenta.obtenerSaldo()); // 1500
Enter fullscreen mode Exit fullscreen mode

Explicación:
El atributo saldo está protegido y solo puede ser modificado mediante métodos públicos.

Herencia

Permitir que una clase hija herede atributos y métodos de una clase padre.

class Animal {
  nombre: string;
  constructor(nombre: string) {
    this.nombre = nombre;
  }

  hacerSonido() {
    console.log("Algún sonido...");
  }
}

class Perro extends Animal {
  hacerSonido() {
    console.log("Guau!");
  }
}

const miPerro = new Perro("Rex");
miPerro.hacerSonido(); // "Guau!"
Enter fullscreen mode Exit fullscreen mode

Explicación:
Perro hereda de Animal y puede sobreescribir el método hacerSonido.

Polimorfismo

Permitir que diferentes clases respondan de manera distinta a un mismo método.

const animales: Animal[] = [
  new Perro("Rex"),
  new Gato("Michi"),
];

animales.forEach(animal => animal.hacerSonido());
// "Guau!"
// "Miau!"
Enter fullscreen mode Exit fullscreen mode

Explicación:
Aunque ambos son Animal, cada subclase implementa su propia versión de hacerSonido.


2. ¿Qué es SOLID?

SOLID es el acrónimo de cinco principios de diseño para la POO, propuestos por Robert C. Martin (“Uncle Bob”).

Su objetivo es ayudarte a escribir código:

  • Más limpio
  • Más mantenible
  • Más flexible y escalable

Los cinco principios son:

  1. Single Responsibility Principle (SRP) – Responsabilidad Única
  2. Open/Closed Principle (OCP) – Abierto/Cerrado
  3. Liskov Substitution Principle (LSP) – Sustitución de Liskov
  4. Interface Segregation Principle (ISP) – Segregación de Interfaces
  5. Dependency Inversion Principle (DIP) – Inversión de Dependencias

3. Los principios SOLID explicados con ejemplos

1. Principio de Responsabilidad Única (SRP)

Cada clase o componente debe tener una sola responsabilidad.

Mal ejemplo (TypeScript):

class Factura {
  calcularTotal() { /* ... */ }
  imprimir() { /* ... */ }
  guardarEnBD() { /* ... */ }
}
Enter fullscreen mode Exit fullscreen mode

Esta clase hace demasiadas cosas.

Mejor ejemplo (TypeScript):

class FacturaCalculo { calcularTotal() { /* ... */ } }
class FacturaImpresion { imprimir() { /* ... */ } }
class FacturaPersistencia { guardarEnBD() { /* ... */ } }
Enter fullscreen mode Exit fullscreen mode

Frontend con React:

❌ Un componente que muestra y guarda datos al mismo tiempo:

function UserProfile({ user }) {
  // Guarda y muestra datos juntos (mala práctica)
  function saveUser() {
    // lógica de guardado
  }

  return (
    <div>
      <span>{user.name}</span>
      <button onClick={saveUser}>Guardar</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

✅ Separando responsabilidades:

function UserProfileView({ user }) {
  return <span>{user.name}</span>;
}

function UserProfileSave({ user, onSave }) {
  return <button onClick={() => onSave(user)}>Guardar</button>;
}

// Uso
<UserProfileView user={user} />
<UserProfileSave user={user} onSave={saveUser} />
Enter fullscreen mode Exit fullscreen mode

Explicación:
Cada componente tiene una función clara: uno muestra datos, otro los guarda.


2. Principio de Abierto/Cerrado (OCP)

Las clases deben estar abiertas para extensión pero cerradas para modificación.

Mal ejemplo (TypeScript):

class SuperHeroe {
  habilidad: string;
  usarHabilidad() {
    if (this.habilidad === "volar") { /* ... */ }
    // Si agrego más habilidades, tengo que modificar la clase.
  }
}
Enter fullscreen mode Exit fullscreen mode

Mejor ejemplo (TypeScript):

class SuperHeroe {
  usarHabilidad() { /* implementación base */ }
}

class SuperHeroeVolador extends SuperHeroe {
  usarHabilidad() { /* volar */ }
}
Enter fullscreen mode Exit fullscreen mode

React Frontend:

❌ Mal ejemplo (modificando para cada tipo de botón)

function Button({ type, ...props }) {
  if (type === "primary") return <button className="blue" {...props} />;
  if (type === "danger") return <button className="red" {...props} />;
  // Cada vez hay que modificar esta función para agregar un tipo nuevo
}
Enter fullscreen mode Exit fullscreen mode

En vez de modificar un componente para cada variante, extiéndelo o compón con props.

✅ Buen ejemplo (usando composición)

function Button({ children, className, ...props }) {
  return <button className={className} {...props}>{children}</button>;
}

// Extender sin modificar:
function PrimaryButton(props) {
  return <Button className="blue" {...props} />;
}

function DangerButton(props) {
  return <Button className="red" {...props} />;
}

function App() {
  return (
    <div>
      <PrimaryButton onClick={() => alert('Primario')}>
        Botón Primario
      </PrimaryButton>
      <DangerButton onClick={() => alert('¡Cuidado!')}>
        Botón de Peligro
      </DangerButton>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Explicación:
Se pueden crear nuevos tipos de botones sin modificar el componente base.


3. Principio de Sustitución de Liskov (LSP)

Las clases hijas deben poder usarse como sus padres, sin romper la aplicación.

Mal ejemplo (TypeScript):

class Ave { volar() {} }
class Pinguino extends Ave { volar() { throw new Error("No puedo volar"); } }
Enter fullscreen mode Exit fullscreen mode

Si reemplazo Ave por Pinguino, ¡el código se rompe!

Mejor ejemplo:

class Ave {}
class AveVoladora extends Ave { volar() {} }
class Pinguino extends Ave {}
Enter fullscreen mode Exit fullscreen mode

En React:

❌ Mal ejemplo (hijo rompe comportamiento esperado)

function Input({ value, onChange }) {
  return <input value={value} onChange={onChange} />;
}

function ReadOnlyInput(props) {
  // No acepta onChange, rompe el contrato
  return <input value={props.value} readOnly />;
}
Enter fullscreen mode Exit fullscreen mode

✅ Mejor ejemplo

// Componente base: Input editable
function Input({ value, onChange }) {
  return <input value={value} onChange={onChange} />;
}

// Variante: Input solo lectura, pero sigue aceptando onChange para cumplir LSP
function ReadOnlyInput({ value, onChange }) {
  return <input value={value} readOnly onChange={onChange} />;
}

// Componente de formulario que puede alternar entre editable y solo lectura
function Formulario({ readOnly }) {
  const [valor, setValor] = React.useState("Texto inicial");

  // Elegimos el componente según la prop readOnly
  const InputComponent = readOnly ? ReadOnlyInput : Input;

  return (
    <div>
      <label>
        Nombre:
        <InputComponent
          value={valor}
          onChange={e => setValor(e.target.value)}
        />
      </label>
      <p>Valor actual: {valor}</p>
    </div>
  );
}

// Ejemplo de uso en tu app principal
function App() {
  const [soloLectura, setSoloLectura] = React.useState(false);

  return (
    <div>
      <button onClick={() => setSoloLectura(prev => !prev)}>
        {soloLectura ? "Cambiar a editable" : "Cambiar a solo lectura"}
      </button>
      <Formulario readOnly={soloLectura} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Explicación:
ReadOnlyInput sigue aceptando las mismas props, aunque ignore algunas, no rompe el contrato.


4. Principio de Segregación de Interfaces (ISP)

No fuerces a implementar métodos que no se necesitan.

Mal ejemplo (TypeScript):

interface SuperHeroe {
  volar(): void;
  nadar(): void;
}

class Ironman implements SuperHeroe {
  volar() {}
  nadar() { /* Ironman no nada, pero debe implementar igual */ }
}
Enter fullscreen mode Exit fullscreen mode

Mejor ejemplo:

interface Volador { volar(): void; }
interface Nadador { nadar(): void; }

class Ironman implements Volador {
  volar() {}
}
Enter fullscreen mode Exit fullscreen mode

En React:

❌ Mal ejemplo (props innecesarias)

function Animal({ name, fly, swim }) {
  // Algunos animales no vuelan ni nadan
}
Enter fullscreen mode Exit fullscreen mode

✅ Buen ejemplo

function Animal({ name }) {
  return <span>{name}</span>;
}

function FlyingAnimal({ name, fly }) {
  // Solo para animales que vuelan
}

function SwimmingAnimal({ name, swim }) {
  // Solo para animales que nadan
}
Enter fullscreen mode Exit fullscreen mode

Explicación:
Cada componente recibe solo las props que necesita.


5. Principio de Inversión de Dependencias (DIP)

Las clases deben depender de abstracciones (interfaces), no de implementaciones concretas.

Mal ejemplo (TypeScript):

class Notificador {
  emailService = new EmailService();
  notificar() { this.emailService.enviar(); }
}
Enter fullscreen mode Exit fullscreen mode

Mejor ejemplo:

interface ServicioNotificacion { enviar(): void; }

class Notificador {
  constructor(private servicio: ServicioNotificacion) {}
  notificar() { this.servicio.enviar(); }
}
Enter fullscreen mode Exit fullscreen mode

En React:

❌ Mal ejemplo (componente crea su propia dependencia)

function Notification() {
  const service = new NotificationService();

  function notify() {
    service.send("Hola!");
  }

  return <button onClick={notify}>Notificar</button>;
}
Enter fullscreen mode Exit fullscreen mode

✅ Buen ejemplo (la dependencia se inyecta)

import React from "react";

// Servicio de notificaciones (puede ser email, SMS, etc.)
class NotificationService {
  send(message) {
    alert(`Servicio de notificación: ${message}`);
    // Aquí podrías hacer una llamada a un API, por ejemplo.
  }
}

// Componente que usa el servicio inyectado (DIP)
function Notification({ notificationService }) {
  function notify() {
    notificationService.send("¡Hola usuario!");
  }

  return <button onClick={notify}>Notificar</button>;
}

// Ejemplo de uso en tu aplicación principal
function App() {
  // Instancia concreta del servicio de notificaciones
  const serviceInstance = new NotificationService();

  return (
    <div>
      <h1>Ejemplo de Inversión de Dependencias (DIP) en React</h1>
      <Notification notificationService={serviceInstance} />
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Explicación:
El componente no conoce la implementación concreta, solo usa lo que recibe.


4. Ventajas y desventajas de aplicar SOLID

Ventajas:

  • Código más limpio y modular.
  • Facilita pruebas (testing) y cambios.
  • Escalabilidad y mantenibilidad.

Desventajas:

  • Puede requerir más clases/archivos.
  • A veces añade complejidad innecesaria en proyectos pequeños.

5. Aplicando SOLID en JavaScript y React

Aunque JavaScript no es estrictamente orientado a objetos, puedes aplicar SOLID en frameworks como React:

  • SRP: Crea componentes pequeños y especializados.
  • OCP: Extiende componentes mediante composición y props.
  • LSP: Evita hacer suposiciones de tipos o estructuras internas.
  • ISP: Define props claras y separa componentes según su función.
  • DIP: Pasa datos y comportamientos por props/context, no desde dentro del componente.

Conclusión

Los principios SOLID te ayudan a escribir mejor código: más limpio, modular y fácil de mantener.

Si trabajas en backend (TypeScript) o frontend (React), empezar a aplicar SOLID te ayudará a mejorar la calidad de tus proyectos.

¿Listo para dar el salto? ¡Empieza a aplicar SOLID en tu próximo proyecto y verás la diferencia!

Te invito a visitar mi blog, donde encontrarás más contenido sobre JavaScript, React, CSS, IA, buenas prácticas y mucho más. 👉 johnserrano.co/blog ¡No te lo pierdas!

Gracias por leer. ❤️

Top comments (0)