¿Qué significa SOLID?
SOLID es un acrónimo que representa cinco principios fundamentales del diseño orientado a objetos. Cada letra corresponde a una regla que te ayuda a escribir código más mantenible, escalable, flexible y fácil de probar.
S — Single Responsibility Principle (SRP)
"Una clase debe tener una única razón para cambiar."
Cada módulo, clase o función debe estar enfocada en una sola tarea o responsabilidad. Si tiene más de una, el cambio en una de esas responsabilidades podría afectar el resto, introduciendo bugs o dificultando el mantenimiento.
Ejemplo
Una clase Factura
que calcula totales y guarda en base de datos viola SRP. Es mejor separar:
class CalculadoraFactura {
calcularTotal(factura) { ... }
}
class PersistenciaFactura {
guardar(factura) { ... }
}
¿Por qué es útil?
Facilita el mantenimiento, el testing, y reduce el impacto de los cambios.
O — Open/Closed Principle (OCP)
"El software debe estar abierto para extensión, pero cerrado para modificación."
¿Qué significa?
Puedes extender el comportamiento de una clase sin modificar su código original. Esto se logra usando abstracciones (interfaces o clases base).
Ejemplo
Un sistema de notificaciones que permite agregar nuevos canales (email, SMS, push) sin tener que tocar el código base de Notificador
.
interface CanalNotificacion {
enviar(mensaje: string): void;
}
class Notificador {
constructor(private canal: CanalNotificacion) {}
enviarMensaje(mensaje: string) {
this.canal.enviar(mensaje);
}
}
¿Por qué es útil?
Evita efectos secundarios y errores al modificar código existente. Permite escalar fácilmente.
L — Liskov Substitution Principle (LSP)
"Los objetos de una clase derivada deben poder sustituir a los de su clase base sin alterar el comportamiento del sistema."
¿Qué significa?
Si tienes una clase base y una subclase, deberías poder usar la subclase donde sea que se use la clase base, sin que el sistema se rompa o cambie su comportamiento esperado.
Ejemplo (violando LSP)
class Ave {
volar() {}
}
class Pinguino extends Ave {
volar() { throw new Error("No puedo volar"); }
}
Esto rompe el principio porque Pinguino
no puede comportarse como Ave
. Mejor usar una abstracción más precisa.
¿Por qué es útil?
Mejora la consistencia, reutilización y evita bugs sutiles en jerarquías de herencia.
I — Interface Segregation Principle (ISP)
"Los clientes no deberían verse forzados a depender de interfaces que no usan."
¿Qué significa?
Es mejor tener varias interfaces pequeñas y específicas, que una grande y genérica. Así, las clases que las implementan solo se preocupan por lo que realmente usan.
Ejemplo
interface Imprimible {
imprimir(): void;
}
interface Escaneable {
escanear(): void;
}
En vez de:
interface Multifuncional {
imprimir(): void;
escanear(): void;
enviarFax(): void;
}
Un dispositivo que no envía fax no debería implementar un método que no necesita.
¿Por qué es útil?
Evita el "código muerto", mejora la cohesión, y permite más flexibilidad en clases pequeñas y desacopladas.
D — Dependency Inversion Principle (DIP)
"Los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deben depender de abstracciones."
¿Qué significa?
En lugar de que las clases se acoplen directamente unas a otras, deben depender de interfaces o abstracciones. Así, puedes cambiar fácilmente una implementación sin afectar al resto del sistema.
Ejemplo
interface Motor {
encender(): void;
}
class Auto {
constructor(private motor: Motor) {}
arrancar() {
this.motor.encender();
}
}
Ahora puedes cambiar de MotorGasolina
a MotorElectrico
sin modificar Auto
.
¿Por qué es útil?
Promueve el desacoplamiento, mejora el testing (se puede inyectar mocks fácilmente) y hace el sistema más mantenible.
Relación entre los principios
Aunque los principios SOLID pueden estudiarse de forma individual, en la práctica funcionan como un sistema interdependiente. Cada uno apoya y habilita a los demás, y cuando se aplican en conjunto, permiten diseñar sistemas robustos, escalables y sostenibles.
Vamos a revisar cómo se conectan y se refuerzan entre sí, con ejemplos concretos en código.
1. SRP y OCP: Separación de responsabilidades y extensión sin modificación
SRP (Single Responsibility Principle) dice que una clase debe tener una única responsabilidad.
OCP (Open/Closed Principle) dice que el software debe estar abierto para extensión, pero cerrado para modificación.
Cómo se relacionan:
Cuando una clase tiene una única responsabilidad (SRP), es más fácil extender su comportamiento sin modificarla directamente (OCP).
Es decir, SRP permite aplicar OCP de forma segura.
Ejemplo:
Supongamos que tienes una clase que representa un reporte financiero:
class ReporteFinanciero {
generarPDF(datos: any) {
// lógica para generar el reporte en PDF
}
enviarPorCorreo(destinatario: string) {
// lógica para enviar el correo
}
}
Esta clase viola SRP porque tiene dos responsabilidades: generar el reporte y enviarlo.
Esto también impide cumplir con OCP, porque si queremos cambiar la forma de envío (por ejemplo, enviar por Slack en vez de correo), tendríamos que modificar esta clase.
Separando responsabilidades:
class GeneradorReporte {
generarPDF(datos: any) {
// lógica del PDF
}
}
class EnviadorCorreo {
enviar(destinatario: string, archivo: any) {
// lógica del correo
}
}
2. LSP e ISP: Jerarquías coherentes y contratos específicos
LSP (Liskov Substitution Principle) establece que las subclases deben poder sustituir a sus superclases sin alterar el comportamiento esperado.
ISP (Interface Segregation Principle) indica que las clases no deben estar forzadas a implementar métodos que no necesitan.
Cómo se relacionan:
Cuando diseñamos interfaces más pequeñas y específicas (ISP), las implementaciones pueden ajustarse mejor a su propósito, evitando violar el contrato de sustitución (LSP).
Ejemplo:
Supongamos que tenemos esta interfaz genérica:
interface DispositivoMultifuncional {
imprimir(): void;
escanear(): void;
enviarFax(): void;
}
Y la siguiente clase:
class ImpresoraBasica implements DispositivoMultifuncional {
imprimir() {
// imprime
}
escanear() {
// escanea
}
enviarFax() {
throw new Error("No puedo enviar fax");
}
}
Aquí, ImpresoraBasica viola LSP, ya que no puede comportarse como se espera de la interfaz. También viola ISP, porque está forzada a implementar métodos que no necesita.
Aplicando ISP:
interface Impresora {
imprimir(): void;
}
interface Escaner {
escanear(): void;
}
interface Fax {
enviarFax(): void;
}
class ImpresoraBasica implements Impresora {
imprimir() {
// implementación válida
}
}
Ahora la clase solo implementa lo que realmente necesita, y cualquier uso de esa interfaz será coherente con LSP.
3. DIP como principio integrador
DIP (Dependency Inversion Principle) indica que los módulos de alto nivel (como la lógica de negocio) no deben depender de implementaciones concretas, sino de abstracciones (interfaces o contratos).
Cómo se relaciona con el resto:
- DIP facilita SRP, ya que las clases pueden centrarse en su lógica sin preocuparse por los detalles de sus dependencias.
- DIP habilita OCP, porque las implementaciones concretas se pueden intercambiar sin modificar el código dependiente.
- DIP se apoya en ISP y LSP, ya que requiere interfaces bien definidas y jerarquías coherentes.
Ejemplo:
Supongamos que tienes una clase que envía notificaciones:
class Notificador {
enviarCorreo(destinatario: string, mensaje: string) {
// lógica para enviar correo
}
}
Esto acopla directamente la lógica a la implementación concreta (correo). Cambiar el medio de envío requeriría modificar esta clase.
Aplicando DIP:
interface CanalNotificacion {
enviar(destinatario: string, mensaje: string): void;
}
class Notificador {
constructor(private canal: CanalNotificacion) {}
notificar(destinatario: string, mensaje: string) {
this.canal.enviar(destinatario, mensaje);
}
}
class Correo implements CanalNotificacion {
enviar(destinatario: string, mensaje: string) {
// lógica de correo
}
}
class SMS implements CanalNotificacion {
enviar(destinatario: string, mensaje: string) {
// lógica de SMS
}
}
Ahora el sistema puede cambiar de medio sin modificar Notificador
. Este diseño:
- Aplica DIP: el notificador depende de una interfaz.
- Cumple SRP: cada clase tiene una única responsabilidad.
- Permite OCP: podemos agregar nuevas clases sin modificar las existentes.
- Aplica ISP: cada clase implementa solo los métodos que necesita.
- Cumple LSP: cualquier clase
CanalNotificacion
puede sustituir a otra sin alterar el comportamiento.
¿Cómo se aplican estos principios en arquitecturas modernas?
En arquitecturas como Clean Architecture, Hexagonal Architecture, o DDD, estos principios no son opcionales: son su base.
Por ejemplo:
- En Clean Architecture, DIP se refleja en que los detalles dependen del dominio, y no al revés.
- En sistemas de microservicios, SRP y OCP ayudan a que cada servicio tenga un único propósito, pero pueda extenderse sin romper el contrato.