Los principios SOLID son la base del código limpio y la pregunta obligada en cualquier entrevista técnica para puestos senior. Pero se suelen explicar como reglas de memoria, cuando en realidad son criterios de revisión: cinco preguntas que te haces frente a cualquier clase o función para decidir si el código va a sobrevivir a la siguiente feature.
Hoy que la inteligencia artificial genera código por ti, saber responder esas cinco preguntas es lo que te separa de quien solo copia y pega. La IA teclea rápido, pero el arquitecto que revisa y manda eres tú.
Para no perderte en la teoría, así queda el resumen completo:
| Principio | Regla en una línea | Señal típica de que lo violas |
|---|---|---|
| S — Responsabilidad única | Una clase hace una sola cosa y tiene una sola razón para cambiar | Clases Util, Helper o Manager que hacen de todo |
| O — Abierto y cerrado | Abierto para agregar, cerrado para modificar | if o switch encadenados por tipo de cliente |
| L — Sustitución de Liskov | El hijo puede reemplazar al padre sin romper nada | throw en una clase hija para métodos del padre |
| I — Segregación de interfaces | Interfaces pequeñas y específicas | Implementadores con métodos vacíos o que lanzan errores |
| D — Inversión de dependencias | Depende de abstracciones, no de implementaciones | new de proveedores concretos dentro de la clase |
S: Responsabilidad única
La regla es simple: un archivo o clase debe hacer una sola cosa y tener una sola razón para cambiar.
Imagina un restaurante donde el chef cocina, cobra en la caja y además lava los baños. Si un día se lastima lavando el baño, la cocina se detiene por completo y el restaurante quiebra por mezclar tareas.
En tu código, el error es tener una clase Usuario que guarda los datos en la base de datos y además manda correos electrónicos. Si el proveedor de correos falla y tienes que modificar la clase, podrías romper el registro de usuarios por accidente. Dos razones para cambiar dentro de una misma clase: dos maneras de romperla.
// Antes: la clase hace dos cosasclass Usuario { guardar() { /* INSERT a la base de datos */ } enviarEmailBienvenida() { /* SMTP al proveedor de correos */ }}
// Después: una clase, una responsabilidadclass Usuario { guardar() { /* INSERT a la base de datos */ }}
class EmailService { enviarBienvenida(usuario: Usuario) { /* SMTP al proveedor de correos */ }}Señales de que lo estás violando: clases con nombres genéricos como Util, Helper o Manager; métodos que mezclan lógica de negocio con detalles técnicos (SQL, HTTP, SMTP); tests que necesitan cinco mocks solo para instanciar la clase; la clase cambia cada vez que cambia cualquier cosa del sistema.
Criterio senior: el umbral no es “número de métodos” sino razones para cambiar. No conviertas una clase de cuarenta líneas en veinte archivos de una línea: eso es sobre-ingeniería, no SOLID. Una clase con dos métodos que comparten la misma razón de cambio no se parte.
O: Abierto y cerrado
Tu código debe estar abierto para agregarle cosas nuevas, pero cerrado a modificar lo que ya funciona.
Piensa en una cafetera de cápsulas. Si quieres un café de vainilla, no desarmas la máquina ni le cambias el motor. Simplemente le insertas una cápsula nueva. La máquina no se toca.
En programación, el problema clásico es tener una función de descuentos llena de condicionales if para clientes VIP o clientes nuevos. Si tu jefe te pide agregar el descuento de Black Friday, tendrías que abrir y modificar ese código viejo, arriesgándote a meter bugs en los descuentos que ya funcionan.
// Antes: cada descuento nuevo obliga a editar la misma funciónfunction calcularDescuento(precio: number, tipo: string): number { if (tipo === 'vip') return precio * 0.9; if (tipo === 'nuevo') return precio * 0.95; return precio;}// Después: el descuento nuevo es una clase que se enchufa, sin tocar lo demásinterface Descuento { aplicar(precio: number): number;}
class DescuentoVIP implements Descuento { aplicar(precio: number) { return precio * 0.9; }}
class DescuentoBlackFriday implements Descuento { aplicar(precio: number) { return precio * 0.7; }}Señales de que lo estás violando: cadenas de if o switch que deciden el comportamiento según un tipo; cada feature nueva te obliga a entrar a la misma función; el test de la función cambia cada vez que agregas un caso nuevo; tienes que entender toda la función antes de tocar un solo caso.
Criterio senior: la abstracción solo se paga sola si la variación existe de verdad. Con dos tipos de descuento fijos, un if es más simple y correcto que una jerarquía de clases. El punto de equilibrio lo conoces tú: si llevas tres descuentos y sabes que van a seguir llegando, ahí sí abstrae.
L: Sustitución de Liskov
Una clase hija siempre debe poder reemplazar a su clase padre sin que la aplicación explote.
Imagina que pides un Uber seleccionando la categoría vehículo. Esperas un auto, pero la app te manda un tractor. Es un vehículo, sí, pero no puede ir rápido por la autopista. El viaje se arruinó porque el hijo no cumplió lo que el padre prometía.
En tu código, imagina que tienes una clase padre Documento con las funciones leer y escribir. Luego creas una clase hija DocumentoDeSoloLectura. Si tu sistema intenta usar escribir en ese documento, la aplicación va a colapsar: el hijo heredó un contrato que no puede cumplir.
// Antes: la hija rompe el contrato del padreclass Documento { leer() { /* ... */ } escribir() { /* ... */ }}
class DocumentoDeSoloLectura extends Documento { escribir() { throw new Error('Este documento no se puede modificar'); }}// Después: nadie promete lo que no puede cumplirclass DocumentoLegible { leer() { /* ... */ }}
class DocumentoEscribible extends DocumentoLegible { escribir() { /* ... */ }}Señales de que lo estás violando: una clase hija que lanza throw o no implementa métodos del padre; instanceof esparcido por el código para decidir el flujo según la clase concreta; funciones que preguntan “¿este objeto puede hacer X?” antes de usarlo.
Criterio senior: la herencia es el mecanismo más barato de abusar. Cuando dudes, prefiere la composición: Documento tiene un lector y tiene un escritor opcional, en lugar de prometerlo por herencia. Si la jerarquía te obliga a negar comportamientos heredados, estás forzando una relación padre-hijo que no existe en la realidad.
I: Segregación de interfaces
Nunca obligues a tu código a depender de funciones que no va a usar.
Es como tener un control remoto de 80 botones para la tele, cuando tú solo usas tres: prender, volumen y cambiar canal. Los otros 77 son pura basura visual.
En el código, esto pasa cuando creas una interfaz gigante llamada DispositivoInteligente que te obliga a programar las funciones llamar y tomarFoto. Si se la aplicas a un reloj básico sin cámara, el sistema te obligará a dejar la función de la foto vacía o con un error.
// Antes: el reloj firma un contrato que no puede cumplirinterface DispositivoInteligente { llamar(): void; tomarFoto(): void;}
class Reloj implements DispositivoInteligente { llamar() { /* ... */ } tomarFoto() { throw new Error('No tengo cámara'); }}// Después: cada dispositivo firma solo lo que de verdad haceinterface DispositivoConLlamadas { llamar(): void;}
interface DispositivoConCamara { tomarFoto(): void;}
class Reloj implements DispositivoConLlamadas { llamar() { /* ... */ }}Señales de que lo estás violando: interfaces con muchos métodos donde los implementadores dejan métodos vacíos o con throw; clases que dependen de una interfaz grande solo para usar un par de métodos; cada cambio en la interfaz obliga a tocar todos los implementadores.
Criterio senior: esta es la “interfaz de rol”. En vez de una interfaz Animal con volar, nadar y correr, tienes Volador, Nadador y Corredor, y cada clase implementa las que le tocan. El costo de las interfaces pequeñas es que son más, así que crea una nueva solo cuando un cliente distinto la necesita.
D: Inversión de dependencias
Tu sistema principal jamás debe amarrarse directamente a una herramienta externa.
Piensa en un piloto de Fórmula 1. Él no depende de un modelo de auto específico, depende de la abstracción de un volante. Si le cambian el coche, simplemente toma el nuevo volante y arranca.
Si en tu tienda online escribes el código amarrado directamente a la pasarela de pagos de Stripe, el día que Stripe se caiga o cambies a PayPal tendrás que reescribir todo tu proyecto.
// Antes: la tienda depende de un proveedor concretoclass Tienda { pasarela: Stripe;
constructor() { this.pasarela = new Stripe(); }
cobrar(monto: number) { return this.pasarela.cargar(monto); }}// Después: la tienda depende de una abstracción, el proveedor se inyectainterface PasarelaDePago { cargar(monto: number): boolean;}
class Stripe implements PasarelaDePago { cargar(monto: number) { /* ... */ return true; }}
class PayPal implements PasarelaDePago { cargar(monto: number) { /* ... */ return true; }}
class Tienda { constructor(private pasarela: PasarelaDePago) {}
cobrar(monto: number) { return this.pasarela.cargar(monto); }}Señales de que lo estás violando: new de dependencias concretas dentro de la clase; pruebas que tocan la red o la base de datos real porque no hay forma de inyectar un falso; cambiar de proveedor externo implica tocar toda la aplicación.
Criterio senior: la inyección por constructor es el patrón más simple y suficiente para el 90% de los casos. No hace falta un contenedor de dependencias ni magia: el objetivo es que quien use Tienda pueda decidir qué pasarela le entrega, no que la clase lo decida por él.
SOLID como checklist para revisar código de IA
Cuando le pides código a una inteligencia artificial, casi siempre te entrega versiones que violan estos principios: clases que hacen de todo, if encadenados por tipo, dependencias creadas con new dentro de la clase. No es malicia: un modelo tiende a escribir el código más directo que resuelve el caso que le pediste.
Por eso SOLID se vuelve tu checklist de review, no un conjunto de reglas para escribir. Frente a cualquier código que te entregue la IA (o un compañero, o tu yo de hace tres meses), recorre las cinco preguntas:
- S — ¿Esta clase tiene más de una razón para cambiar?
- O — ¿Agregar un caso nuevo obliga a editar lo que ya funciona?
- L — ¿Hay hijos que heredan comportamientos que no pueden cumplir?
- I — ¿Alguien depende de funciones que no usa?
- D — ¿Los proveedores externos se inyectan o se crean dentro?
Pídele a la IA un refactor dirigido a un principio a la vez (“refactoriza esta clase para cumplir la responsabilidad única sin cambiar su comportamiento público”) y revisa que el resultado no haya movido el problema a otro lado. El modelo escribe rápido; tú decides si lo que escribió va a sobrevivir al cambio siguiente.
Cuándo NO seguir SOLID
SOLID es una guía para código que va a evolucionar, no un mandamiento universal. Hay casos donde aplicarlo es puro ruido:
- Scripts de una sola vez. Un script que corre una migración y se borra no necesita abstracciones. El código muerto pesa más que el código duplicado.
- Prototipos y código descartable. Si no sabes si la feature sobrevive, primero haz que funcione. Refactoriza cuando sepas que va a quedarse.
- El “rule of three”. No abstraigas hasta la tercera vez que veas el mismo patrón. La primera vez lo haces directo, la segunda ya lo reconoces, la tercera tienes suficiente evidencia para abstraer.
- Dominios estables. Si la regla de negocio nunca cambia, un
ifes más honesto que una jerarquía de clases que nadie volverá a tocar.
El criterio senior de fondo es este: la mejor abstracción es la que se paga sola. Cada capa de indirección cuesta lectura, depuración y navegación. Solo vale la pena si ahorra más de lo que cuesta. Un código que respeta SOLID cuando no lo necesita es código sobre-ingenierizado; eso también se nota en una entrevista, y no a favor tuyo.
Cuál principio priorizar en una app de negocio
No todos los principios tienen el mismo peso en un sistema típico de negocio: tablas, formularios, reportes, una API y reglas de dominio. En mi experiencia, el retorno está desbalanceado:
- S y D dan el mayor retorno casi siempre. La responsabilidad única hace el código testeable y navegable; la inversión de dependencias hace que los cambios de proveedor y las pruebas sean baratos. Empieza por ahí.
- O paga cuando tu dominio cambia seguido: descuentos, tarifas, reglas de validación, pasarelas de pago. Si tu negocio agrega reglas cada sprint, es tu segundo mejor amigo.
- I y L aparecen cuando hay jerarquías de tipos: documentos, dispositivos, cuentas. En apps simples muchas veces nunca las necesitas, y está bien.
Aplicar los cinco a todo, siempre, es el error de quien aprendió la teoría pero no el oficio. Aplicar los correctos donde el código de verdad evoluciona es el comportamiento de un senior.
El valor real de SOLID hoy
La inteligencia artificial te va a ayudar a teclear el código más rápido, pero saber evaluar si ese código respeta estos cinco principios es lo que te convierte en el arquitecto del proyecto y te hace ganar como senior.
Ningún principio te va a salvar de un producto que nadie quiere. Pero cuando el producto crece y el código es la razón por la que cada cambio toma el doble, ahí es donde estos criterios separan el proyecto que se mantiene del que se reescribe.
¿Estás aplicando estos principios en tu código diario o estás dejando que la IA decida por ti? Si quieres que profundice en alguno de ellos — o revisar juntos un caso real de tu código — dime cuál te interesa y lo desarrollamos sin dar tantas vueltas.

