Principios SOLID
SOLID es un acrónimo que agrupa cinco principios de diseño orientado a objetos formulados por Robert C. Martin ("Uncle Bob"). Su objetivo no es una lista de reglas mecánicas sino una guía para escribir código que sea fácil de entender, extender y mantener a lo largo del tiempo sin acumular deuda técnica.
En Wiedii aplicamos SOLID a nivel de módulos, clases, servicios y funciones. Más allá del paradigma orientado a objetos, estos principios tienen equivalentes directos en diseño funcional y en arquitectura de microservicios.
Los cinco principios:
- S — Single Responsibility Principle (Responsabilidad única)
- O — Open/Closed Principle (Abierto/cerrado)
- L — Liskov Substitution Principle (Sustitución de Liskov)
- I — Interface Segregation Principle (Segregación de interfaces)
- D — Dependency Inversion Principle (Inversión de dependencias)
S — Single Responsibility Principle
"A module should have one, and only one, reason to change." — Robert C. Martin
Qué significa
Un módulo, clase o función debe tener una sola razón para cambiar. Esto no significa "haz una sola cosa" en sentido estricto, sino que toda la lógica que contiene responde a las necesidades de un solo actor — un equipo, un caso de uso, una parte del negocio.
La prueba práctica: si te preguntan por qué cambió este archivo y la respuesta puede ser "por el equipo de facturación o por el equipo de RRHH", hay dos responsabilidades mezcladas.
❌ Violación
class Employee:
def calculate_pay(): # lógica de nómina → responsabilidad de RRHH
def save_to_database(): # persistencia → responsabilidad de infraestructura
def generate_report(): # reportes → responsabilidad de contabilidad
Tres razones distintas para cambiar. Si cambia el formato del reporte, el cálculo de nómina queda expuesto al riesgo de un error accidental.
✅ Aplicación correcta
class PayCalculator:
def calculate_pay(employee): ... # RRHH lo modifica
class EmployeeRepository:
def save(employee): ... # infraestructura lo modifica
class EmployeeReportGenerator:
def generate(employee): ... # contabilidad lo modifica
Cada clase tiene una única razón para cambiar. Los cambios en una no afectan a las otras.
Señales de alerta
- Clases o módulos de más de 300 líneas que hacen "de todo".
- Nombres genéricos como
Manager,Helper,Util,Serviceque esconden múltiples responsabilidades. - Un archivo que importa herramientas de tres capas distintas (base de datos, HTTP, lógica de negocio) a la vez.
- En code review: "si cambias esta función, tienes que revisar también X, Y y Z".
En Wiedii
Aplica especialmente a los servicios NestJS y a los módulos de las aplicaciones Astro/Next. Cada service debe tener una sola razón de cambio. Si un service empieza a hacer llamadas HTTP y cálculos de negocio y formateo de respuesta, es señal de refactor.
O — Open/Closed Principle
"Software entities should be open for extension, but closed for modification." — Bertrand Meyer
Qué significa
Puedes añadir nuevo comportamiento a un módulo sin necesidad de modificar su código existente. La extensión se hace mediante herencia, composición, inyección de estrategias o plugins — no editando el código fuente que ya funciona y tiene tests.
El riesgo que este principio evita: cada vez que modificas código que ya funciona, introduces la posibilidad de romper algo. OCP minimiza ese riesgo haciendo que la extensión no requiera modificación.
❌ Violación
function calculate_discount(order, customer_type):
if customer_type == "vip":
return order.total * 0.20
elif customer_type == "regular":
return order.total * 0.05
elif customer_type == "new": # ← nuevo tipo añadido modificando el código
return order.total * 0.10
Cada nuevo tipo de cliente requiere modificar esta función. Si tiene tests, los tienes que actualizar. Si hay un bug en la modificación, rompes los tipos existentes.
✅ Aplicación correcta
interface DiscountStrategy:
calculate(order) -> amount
class VipDiscount implements DiscountStrategy:
calculate(order): return order.total * 0.20
class RegularDiscount implements DiscountStrategy:
calculate(order): return order.total * 0.05
class NewCustomerDiscount implements DiscountStrategy: # ← extensión sin modificar lo existente
calculate(order): return order.total * 0.10
function calculate_discount(order, strategy: DiscountStrategy):
return strategy.calculate(order)
Añadir un nuevo tipo de descuento es crear una nueva clase que implementa la interfaz, sin tocar calculate_discount.
Señales de alerta
- Cadenas largas de
if/elseoswitchque crecen con cada nueva funcionalidad. - Comentarios del tipo
// añadir aquí el nuevo caso. - Tests que fallan cuando añades funcionalidad nueva (en lugar de solo fallar cuando hay un bug).
En Wiedii
OCP es la base del sistema de skills de Claude: cada skill es una extensión que añade comportamiento sin modificar el core de dev-policies. También aplica al diseño de pipelines CI: cada nuevo tipo de lint, test o deploy es un job adicional, no una modificación del job existente.
L — Liskov Substitution Principle
"If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of the program." — Barbara Liskov
Qué significa
Si B es una subclase de A, entonces cualquier código que funcione con A debe funcionar igual de bien con B — sin sorpresas, sin excepciones adicionales, sin comportamiento inesperado.
En términos prácticos: una subclase no puede restringir lo que la clase base promete ni cambiar las precondiciones que el código cliente asume.
❌ Violación clásica (el rectángulo y el cuadrado)
class Rectangle:
set_width(w): self.width = w
set_height(h): self.height = h
area(): return self.width * self.height
class Square extends Rectangle:
set_width(w): self.width = w; self.height = w # ← rompe la semántica de Rectangle
set_height(h): self.width = h; self.height = h
function resize(rect: Rectangle):
rect.set_width(5)
rect.set_height(10)
assert rect.area() == 50 # ← falla si rect es un Square
Un Square no puede sustituir a Rectangle aunque parezca lógico desde el dominio. El cuadrado viola la promesa de Rectangle de que ancho y alto son independientes.
✅ Aplicación correcta
interface Shape:
area() -> number
class Rectangle implements Shape:
area(): return self.width * self.height
class Square implements Shape:
area(): return self.side * self.side
Ambas implementan la misma interfaz sin relación de herencia. El código cliente trabaja con Shape y cualquier implementación es intercambiable.
❌ Otro patrón de violación frecuente
class ReadOnlyRepository extends Repository:
save(entity):
raise NotImplementedError("Este repositorio es de solo lectura")
El código que usa Repository asume que save funciona. Una subclase que lanza una excepción donde la base clase promete un resultado viola LSP.
Señales de alerta
- Subclases que sobreescriben métodos para lanzar
NotImplementedErroro equivalente. - Código cliente que hace
if isinstance(obj, SubclaseEspecifica)para tratar el subtipo distinto. - Tests que pasan con la clase base pero fallan con la subclase.
En Wiedii
LSP es crítico en el diseño de repositorios y adaptadores. Si tienes un UserRepository y una versión CachedUserRepository, el segundo debe ser un drop-in replacement sin que el código que usa el repositorio lo note.
I — Interface Segregation Principle
"Clients should not be forced to depend upon interfaces that they do not use." — Robert C. Martin
Qué significa
Es mejor tener muchas interfaces pequeñas y específicas que una sola interfaz grande y genérica. El cliente solo debe conocer los métodos que realmente usa.
Cuando una interfaz es demasiado amplia, los clientes quedan acoplados a métodos que no necesitan. Un cambio en un método que no usan puede obligarlos a recompilarse o actualizarse innecesariamente.
❌ Violación
interface Worker:
work()
eat()
sleep()
class HumanWorker implements Worker:
work(): ...
eat(): ...
sleep(): ...
class RobotWorker implements Worker:
work(): ...
eat(): raise NotImplementedError("Los robots no comen") # ← forzado
sleep(): raise NotImplementedError("Los robots no duermen") # ← forzado
RobotWorker está forzado a implementar métodos que no le corresponden. Violación de LSP + ISP juntos.
✅ Aplicación correcta
interface Workable:
work()
interface Feedable:
eat()
interface Restable:
sleep()
class HumanWorker implements Workable, Feedable, Restable:
work(): ...
eat(): ...
sleep(): ...
class RobotWorker implements Workable:
work(): ...
Cada clase implementa solo las interfaces que le son pertinentes. El código que solo necesita Workable no sabe nada de eat() ni sleep().
Señales de alerta
- Interfaces con más de 5-7 métodos donde los implementadores dejan varios como
passoraise NotImplementedError. - Clientes que importan una interfaz grande pero solo usan dos de sus diez métodos.
- Mocks de tests que tienen que implementar 15 métodos para testear uno solo.
En Wiedii
ISP aplica al diseño de contratos entre servicios. Si un microservicio expone una interfaz de 20 endpoints pero cada cliente solo usa 3, conviene agrupar los endpoints por caso de uso y exponer subcontratos. También aplica en el diseño de los repositorios: separar ReadRepository de WriteRepository permite que los casos de uso de solo lectura no dependan de la capacidad de escritura.
D — Dependency Inversion Principle
"High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions." — Robert C. Martin
Qué significa
Los módulos de alto nivel (lógica de negocio, casos de uso) no deben conocer los detalles de los módulos de bajo nivel (base de datos, HTTP, sistema de archivos). Ambos deben depender de abstracciones (interfaces, contratos). Son los detalles (implementaciones concretas) los que deben adaptarse a las abstracciones, no al revés.
El beneficio práctico: puedes cambiar la base de datos, el proveedor de email o la librería HTTP sin tocar ni una línea de lógica de negocio.
❌ Violación
class OrderService:
def __init__(self):
self.db = PostgreSQLDatabase() # ← acoplado a implementación concreta
self.mailer = SendGridMailer() # ← acoplado a implementación concreta
def place_order(order):
self.db.save(order)
self.mailer.send_confirmation(order)
OrderService (alto nivel) depende directamente de PostgreSQLDatabase y SendGridMailer (bajo nivel). Para testear place_order necesitas una base de datos real y una cuenta de SendGrid.
✅ Aplicación correcta
interface OrderRepository:
save(order)
interface Mailer:
send_confirmation(order)
class OrderService:
def __init__(self, repo: OrderRepository, mailer: Mailer):
self.repo = repo
self.mailer = mailer
def place_order(order):
self.repo.save(order)
self.mailer.send_confirmation(order)
# En producción:
service = OrderService(PostgreSQLOrderRepository(), SendGridMailer())
# En tests:
service = OrderService(InMemoryOrderRepository(), FakeMailer())
OrderService no sabe qué base de datos ni qué servicio de email existe. Solo conoce los contratos. Cambiar de PostgreSQL a otro motor es cambiar el constructor, no la lógica de negocio.
Señales de alerta
importde librerías de infraestructura (ORMs, clientes HTTP, SDKs de proveedores) directamente en módulos de lógica de negocio.- Tests unitarios que requieren levantar una base de datos o llamar a un API externo.
- La frase "no puedo testear esto sin el entorno completo".
En Wiedii
DIP es el principio que permite mockear dependencias en los tests unitarios. Toda lógica de negocio debe recibir sus dependencias por inyección (constructor, parámetros de función, o contenedor DI). Los adaptadores concretos (PostgreSQL, Redis, S3, SendGrid) viven en la capa de infraestructura y se conectan al arrancar la aplicación.
Relación entre los cinco principios
Los principios SOLID no son independientes — se refuerzan mutuamente:
- SRP define qué debe hacer cada módulo.
- OCP define cómo se extiende sin romperse.
- LSP garantiza que las extensiones son intercambiables.
- ISP mantiene las interfaces pequeñas para que LSP sea manejable.
- DIP asegura que todo lo anterior es testeable e independiente de detalles.
Un código que cumple los cinco principios es, por construcción, más fácil de testear, extender y mantener.
Cuándo no aplicar SOLID mecánicamente
SOLID es una guía, no una ley. Aplicarlo a ciegas puede generar over-engineering:
- En un script de 50 líneas que se ejecuta una vez y se descarta, SOLID es ruido.
- En el prototipado rápido, la inversión de dependencias puede postergar decisiones de diseño hasta que el dominio esté claro.
- DIP sin un contenedor DI en un proyecto pequeño puede generar más complejidad que valor.
La regla práctica de Wiedii: aplica SOLID cuando el código va a ser mantenido por más de una persona o va a cambiar más de dos veces. Si el código es desechable o extremadamente simple, KISS prevalece.
Documentos relacionados
- DRY — No te repitas (complementa SRP y OCP)
- KISS — Mantén la simplicidad (equilibrio con SOLID)
- politicas-core — Políticas de desarrollo Wiedii
- repo-setup — Setup de repositorios con estas prácticas integradas