¿Qué son los principios SOLID? Guía para principiantes de los 5 principios fundamentales del diseño orientado a objetos [Con ejemplos en Python]

¿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:

LetraPrincipioIdea central
SResponsabilidad ÚnicaUna clase, una razón para cambiar
OAbierto/CerradoAbre para extender, cierra para modificar
LSustitución de LiskovLas subclases deben ser sustituibles por sus bases
ISegregación de InterfacesInterfaces pequeñas y específicas
DInversión de DependenciasDepende 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:

bad_user_manager.py
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:

AspectoAntes de SOLIDDespués de SOLID
CambiosUn cambio rompe varias partesCambios localizados y predecibles
TestingDifícil mockear dependenciasInyección de dependencias facilita tests
LecturaClases de cientos de líneasClases pequeñas y enfocadas
ExtensiónModificar código existenteAñ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

bad_srp.py
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")
💡 Tip

Si no puedes describir la responsabilidad de una clase en una sola frase, probablemente esté violando el SRP.

Ejemplo bueno: responsabilidades separadas

good_srp.py
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:

ClaseResponsabilidad
UserRepositoryPersistencia de usuarios
EmailServiceEnvío de emails
UserExporterExportación a CSV
UserServiceOrquestació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

bad_ocp.py
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")
⚠️ Error común

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

good_ocp.py
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

bad_lsp.py
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
⚠️ Error común

Quien espera un Bird que vuele se encontrará con una excepción si recibe un Penguin.

Ejemplo bueno: jerarquía que respeta el contrato

good_lsp.py
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

bad_isp.py
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

good_isp.py
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

bad_dip.py
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

good_dip.py
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")
💡 Tip

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

CriterioSin SOLIDCon SOLID
AcoplamientoAlto: clases dependen de implementaciones concretasBajo: dependencias inyectadas o abstracciones
CohesiónBaja: clases hacen demasiadas cosasAlta: una responsabilidad por clase
TestabilidadDifícil: requiere bases de datos, redes, etc.Fácil: mocks inyectables
MantenibilidadCambios en cascadaCambios localizados
ExtensibilidadModificar código existenteAñ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

PrioridadPrincipioMotivo
1S — Responsabilidad ÚnicaEl más intuitivo y con impacto inmediato
2O — Abierto/CerradoExtensión sin modificación reduce riesgos
3D — Inversión de DependenciasClave para testing y flexibilidad
4I — Segregación de InterfacesEvita interfaces hinchadas
5L — Sustitución de LiskovMás sutil; se aprecia con experiencia en herencia

Conceptos erróneos frecuentes

MitoRealidad
SOLID = más clases siempreSOLID busca cohesión y bajo acoplamiento; a veces una clase está bien
Hay que aplicar los cinco siempreUsa el sentido común; no forces abstracciones prematuras
SOLID solo aplica a OOP estrictoLos conceptos (SRP, DIP) son útiles también en lenguajes multiparadigma
Más interfaces = mejor diseñoInterfaces 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.

Comments

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *