¿Alguna vez has modificado una función y roto otra parte del código sin querer? ¿O has tenido miedo de añadir una nueva funcionalidad porque no sabías qué más podía fallar? Estos son síntomas clásicos de un diseño de software débil. Los principios SOLID son cinco reglas fundamentales del diseño orientado a objetos que te ayudan a escribir código resiliente al cambio, fácil de mantener y de extender.
En esta guía aprenderás qué son los principios SOLID, por qué importan y cómo aplicarlos con ejemplos prácticos en Python. También verás comparaciones malo-vs-bueno, tablas de referencia y aplicaciones en frameworks web, microservicios y arquitectura limpia. Si estás empezando con Python, te recomendamos nuestra guía de inicio con Python antes de profundizar en patrones de diseño.
Los principios SOLID fueron codificados por Robert C. Martin (Uncle Bob) a principios de los 2000 y siguen siendo la base del diseño orientado a objetos moderno.
Resumen de los 5 principios SOLID
Antes de entrar en detalle, aquí tienes una vista rápida de los cinco principios con enlaces a cada sección:
| Letra | Principio | Idea central |
| S | Responsabilidad Única | Una clase, una razón para cambiar |
| O | Abierto/Cerrado | Abre para extender, cierra para modificar |
| L | Sustitución de Liskov | Las subclases deben ser sustituibles por sus bases |
| I | Segregación de Interfaces | Interfaces pequeñas y específicas |
| D | Inversión de Dependencias | Depende de abstracciones, no de implementaciones |
¿Por qué necesitamos SOLID?
Sin principios de diseño claros, el código tiende a convertirse en un laberinto de dependencias. Un ejemplo típico es la clase «God Class» que hace de todo:
class UserManager:
def create_user(self, name, email): ...
def send_email(self, user, subject, body): ...
def save_to_database(self, user): ...
def validate_email(self, email): ...
def generate_report(self, users): ...
Esta clase tiene demasiadas responsabilidades. Si cambias la lógica de envío de emails, podrías afectar la validación. Si modificas el reporte, podrías romper la persistencia. La siguiente tabla resume el antes y el después de aplicar SOLID:
| Aspecto | Antes de SOLID | Después de SOLID |
| Cambios | Un cambio rompe varias partes | Cambios localizados y predecibles |
| Testing | Difícil mockear dependencias | Inyección de dependencias facilita tests |
| Lectura | Clases de cientos de líneas | Clases pequeñas y enfocadas |
| Extensión | Modificar código existente | Añadir nuevas clases sin tocar las viejas |
S — Principio de Responsabilidad Única (SRP)
Una clase debe tener una sola razón para cambiar. Si una clase hace varias cosas, cualquier cambio en una de ellas puede obligarte a modificarla, con el riesgo de romper el resto.
Ejemplo malo: una clase con múltiples responsabilidades
class UserService:
def __init__(self):
self.db = []
def create_user(self, name, email):
if "@" not in email:
raise ValueError("Invalid email")
user = {"name": name, "email": email}
self.db.append(user)
# Enviar email de bienvenida
print(f"Sending welcome email to {email}")
return user
def export_to_csv(self, filename):
with open(filename, "w") as f:
f.write("name,email\n")
for u in self.db:
f.write(f"{u['name']},{u['email']}\n")
Si no puedes describir la responsabilidad de una clase en una sola frase, probablemente esté violando el SRP.
Ejemplo bueno: responsabilidades separadas
class UserRepository:
def __init__(self):
self.db = []
def save(self, user):
self.db.append(user)
return user
def find_all(self):
return list(self.db)
class EmailService:
def send_welcome(self, email):
print(f"Sending welcome email to {email}")
class UserExporter:
def to_csv(self, users, filename):
with open(filename, "w") as f:
f.write("name,email\n")
for u in users:
f.write(f"{u['name']},{u['email']}\n")
class UserService:
def __init__(self, repo, email_svc, exporter):
self.repo = repo
self.email_svc = email_svc
self.exporter = exporter
def create_user(self, name, email):
if "@" not in email:
raise ValueError("Invalid email")
user = {"name": name, "email": email}
self.repo.save(user)
self.email_svc.send_welcome(email)
return user
def export_users(self, filename):
users = self.repo.find_all()
self.exporter.to_csv(users, filename)
Cada clase tiene una responsabilidad clara:
| Clase | Responsabilidad |
| UserRepository | Persistencia de usuarios |
| EmailService | Envío de emails |
| UserExporter | Exportación a CSV |
| UserService | Orquestación del flujo de creación |
O — Principio Abierto/Cerrado (OCP)
Las entidades de software deben estar abiertas para extensión pero cerradas para modificación. Debes poder añadir nuevos comportamientos sin cambiar el código existente.
Ejemplo malo: modificar código para cada nuevo tipo
def calculate_area(shape):
if shape["type"] == "circle":
return 3.14159 * shape["radius"] ** 2
elif shape["type"] == "rectangle":
return shape["width"] * shape["height"]
elif shape["type"] == "triangle":
return 0.5 * shape["base"] * shape["height"]
else:
raise ValueError("Unknown shape")
Añadir un nuevo tipo de forma obliga a modificar la función y todos los lugares que la llaman.
Ejemplo bueno: extensión por herencia o composición
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Triangle(Shape):
def __init__(self, base, height):
self.base = base
self.height = height
def area(self):
return 0.5 * self.base * self.height
Para añadir un nuevo tipo de forma, creas una nueva clase que extiende Shape sin tocar las existentes.
L — Principio de Sustitución de Liskov (LSP)
Los objetos de una subclase deben poder sustituir a los de la clase base sin alterar el comportamiento esperado del programa. Si el código que usa la clase base funciona con la subclase, se cumple el LSP.
Ejemplo malo: Penguin que hereda de Bird
class Bird:
def fly(self):
return "Flying"
class Penguin(Bird):
def fly(self):
raise NotImplementedError("Penguins cannot fly")
def make_bird_fly(bird: Bird):
return bird.fly() # Falla si bird es Penguin
Quien espera un Bird que vuele se encontrará con una excepción si recibe un Penguin.
Ejemplo bueno: jerarquía que respeta el contrato
from abc import ABC, abstractmethod
class Bird(ABC):
@abstractmethod
def move(self):
pass
class Sparrow(Bird):
def move(self):
return "Flying"
class Penguin(Bird):
def move(self):
return "Swimming"
def make_bird_move(bird: Bird):
return bird.move() # Funciona con cualquier Bird
Ambas subclases cumplen el contrato de Bird: pueden moverse. La sustitución es segura.
I — Principio de Segregación de Interfaces (ISP)
Los clientes no deberían verse obligados a depender de interfaces que no utilizan. Es mejor tener muchas interfaces pequeñas y específicas que una grande y monolítica.
Ejemplo malo: interfaz que obliga a implementar métodos innecesarios
from abc import ABC, abstractmethod
class Worker(ABC):
@abstractmethod
def work(self):
pass
@abstractmethod
def eat(self):
pass
class Human(Worker):
def work(self):
return "Working"
def eat(self):
return "Eating"
class Robot(Worker):
def work(self):
return "Working"
def eat(self):
raise NotImplementedError("Robots do not eat")
El robot no come, pero la interfaz Worker le obliga a implementar eat.
Ejemplo bueno: interfaces segregadas
from abc import ABC, abstractmethod
class Workable(ABC):
@abstractmethod
def work(self):
pass
class Eatable(ABC):
@abstractmethod
def eat(self):
pass
class Human(Workable, Eatable):
def work(self):
return "Working"
def eat(self):
return "Eating"
class Robot(Workable):
def work(self):
return "Working"
Cada clase implementa solo las interfaces que necesita.
D — Principio de Inversión de Dependencias (DIP)
Los módulos de alto nivel no deben depender de los de bajo nivel. Ambos deben depender de abstracciones. Las abstracciones no deben depender de los detalles; los detalles deben depender de las abstracciones.
Ejemplo malo: dependencia directa de MySQL
class MySQLConnection:
def query(self, sql):
return []
class UserService:
def __init__(self):
self.db = MySQLConnection()
def get_users(self):
return self.db.query("SELECT * FROM users")
Si quieres cambiar a PostgreSQL o usar un mock en tests, tienes que modificar UserService.
Ejemplo bueno: dependencia de abstracción
from abc import ABC, abstractmethod
class Database(ABC):
@abstractmethod
def query(self, sql):
pass
class MySQLConnection(Database):
def query(self, sql):
return []
class PostgreSQLConnection(Database):
def query(self, sql):
return []
class UserService:
def __init__(self, db: Database):
self.db = db
def get_users(self):
return self.db.query("SELECT * FROM users")
La inyección de dependencias es la técnica práctica más común para aplicar el DIP. Puedes inyectar una implementación real en producción y un mock en tests.
Comparación antes y después de SOLID
| Criterio | Sin SOLID | Con SOLID |
| Acoplamiento | Alto: clases dependen de implementaciones concretas | Bajo: dependencias inyectadas o abstracciones |
| Cohesión | Baja: clases hacen demasiadas cosas | Alta: una responsabilidad por clase |
| Testabilidad | Difícil: requiere bases de datos, redes, etc. | Fácil: mocks inyectables |
| Mantenibilidad | Cambios en cascada | Cambios localizados |
| Extensibilidad | Modificar código existente | Añadir nuevas clases |
SOLID en la práctica
Los principios SOLID no son teoría abstracta. Se aplican en contextos reales:
• Frameworks web — Django, FastAPI y Flask separan modelos, vistas y controladores. Cada capa tiene responsabilidades claras. Consulta nuestra guía de frameworks web en Python para ver cómo encajan estos patrones.
• Microservicios — Cada servicio tiene una responsabilidad acotada (SRP) y se comunica por interfaces bien definidas (ISP, DIP).
• Clean Architecture / DDD — Las capas internas no dependen de las externas; la inversión de dependencias es central.
• AI/ML — Pipelines de datos, modelos y evaluación se separan para facilitar experimentación.
• Diseño de APIs — Endpoints pequeños y cohesivos, versionado y contratos estables reflejan SOLID a nivel de API.
Orden de aprendizaje recomendado
| Prioridad | Principio | Motivo |
| 1 | S — Responsabilidad Única | El más intuitivo y con impacto inmediato |
| 2 | O — Abierto/Cerrado | Extensión sin modificación reduce riesgos |
| 3 | D — Inversión de Dependencias | Clave para testing y flexibilidad |
| 4 | I — Segregación de Interfaces | Evita interfaces hinchadas |
| 5 | L — Sustitución de Liskov | Más sutil; se aprecia con experiencia en herencia |
Conceptos erróneos frecuentes
| Mito | Realidad |
| SOLID = más clases siempre | SOLID busca cohesión y bajo acoplamiento; a veces una clase está bien |
| Hay que aplicar los cinco siempre | Usa el sentido común; no forces abstracciones prematuras |
| SOLID solo aplica a OOP estricto | Los conceptos (SRP, DIP) son útiles también en lenguajes multiparadigma |
| Más interfaces = mejor diseño | Interfaces innecesarias añaden complejidad; ISP pide interfaces pequeñas y útiles |
Qué aprender después de SOLID
Una vez dominados los principios SOLID, explora patrones de diseño más concretos: Factory, Strategy, Observer, Decorator. También te ayudará profundizar en patrones de manejo de errores en Python, patrones de seguridad y nuestra guía de comparación de lenguajes para ver cómo otros lenguajes implementan estos conceptos.
Preguntas frecuentes
¿SOLID aplica a Python?
Sí. Aunque Python no obliga a usar clases, los principios SOLID son igualmente válidos. SRP y DIP se aplican a funciones y módulos; OCP, LSP e ISP a jerarquías de clases cuando las uses.
¿Puedo violar SOLID a propósito?
En prototipos rápidos o scripts de una sola vez, priorizar la velocidad puede ser razonable. En código que vivirá años o será mantenido por varios desarrolladores, respetar SOLID suele compensar.
¿SOLID hace el código más lento?
No. Las abstracciones (interfaces, inyección) tienen coste mínimo en tiempo de ejecución. El beneficio en mantenibilidad y testing supera ampliamente cualquier micro-overhead.
¿Cómo detecto violaciones de SOLID en código existente?
Señales: clases muy largas, métodos que hacen varias cosas, dependencias hardcodeadas, herencias que rompen el contrato de la clase base. Las herramientas de análisis estático y los code reviews ayudan a detectarlas.
¿SOLID y Clean Code son lo mismo?
No. SOLID son cinco principios de diseño. Clean Code (también de Robert C. Martin) abarca nombres, funciones pequeñas, comentarios y más. Se complementan.
Resumen
Los principios SOLID son cinco reglas que te ayudan a escribir código orientado a objetos más mantenible y extensible:
• S — Una clase, una responsabilidad
• O — Extiende sin modificar
• L — Las subclases sustituyen correctamente a las bases
• I — Interfaces pequeñas y específicas
• D — Depende de abstracciones
Empieza por el SRP y el DIP; tienen el mayor impacto práctico. Para seguir aprendiendo, revisa nuestra guía de inicio con Python, los patrones de manejo de errores y la guía de frameworks web.

Deja una respuesta