Principios SOLID
CategoriasProgramación

Principios SOLID

¿Qué son los principios SOLID?

Los principios SOLID, es un conjunto de cinco principios básicos relacionados con la programación orientada a objetos y diseño de software. Aplicar estos principios ayuda a crear un sistema fácil de mantener, flexible y, por tanto, con mayor vida. Estos principios fueron creados por Robert C. Martin a principios del 2000, sin embargo, no pasan de moda, porque el software creado aplicando estos principios es… SOLIDO 🤪

Perdona… vayamos en serio, veamos los principios…

Los 5 principios SOLID son los siguientes:

1. Single responsibility principle (Principio de responsabilidad única).

“Una clase debe tener únicamente una razón para cambiar”.

Básicamente se traduce en que una clase debe de tener únicamente una responsabilidad, es decir, debe enfocarse en hacer una cosa solamente.

Un ejemplo incorrecto y donde se viola este este principio, sería por ejemplo la siguiente clase:

class Usuarios:
    def crear_usuario(self, nombre, email):
        # Crear usuario
        usuario = {"nombre": nombre, "email": email}
    
        # Enviar correo electrónico de bienvenida
        self.enviar_email_bienvenida(usuario)

    def enviar_email_bienvenida(self, usuario):
        print(f"Enviando correo de bienvenida a {usuario['email']}...")

# Uso
manager = Usuarios()
manager.crear_usuario("Óscar", "oscar@oscardev.net")

¿Por qué es incorrecto?

Sencillamente porque tiene más de una responsabilidad.

Por un lado, maneja la lógica de la creación de usuarios y por otro, se encarga de enviar correos electrónicos. Por tanto, lo que acabamos de hacer (aunque sea un ejemplo muy simple) es sobrecargar la clase con múltiples comportamientos o responsabilidades.

Una sencilla pregunta que nos podemos hacer para saber si violamos o no este principio es: ¿Qué hace la clase?

Si en la explicación de lo que hace la clase, se enumera más de una responsabilidad, ya estamos violando el principio SRP.

¿Y cuál sería el uso correcto?

Pues dividir las responsabilidades en diferentes clases, cada una con una responsabilidad clara.

Por ejemplo:

# Clase que gestiona la creación de usuarios
class Usuarios:
    def __init__(self, email_sender):
        self.email_sender = email_sender

    def create_user(self, nombre, email):
        usuario = {"nombre": nombre, "email": email}
        self.email_sender.enviar_email_bienvenida(usuario)

# Clase para el envio de correos electrónicos
class EmailSender:
    def enviar_email_bienvenida(self, usuario):
        print(f"Enviando correo de bienvenida a {usuario['email']}...")

# Uso
email_sender = EmailSender()
manager = Usuarios(email_sender)
manager.crear_usuario("Óscar", "oscar@correoejemplo.com")

Ahora cada clase tiene una única responsabilidad.

Usuarios gestiona la lógica de creación de usuarios y, EmailSender, se responsabiliza de enviar correos electrónicos.

De esta manera, si queremos cambiar la forma de enviar correos, por ejemplo, se puede hacer sin tocar la lógica de creación de usuarios, lo que hace el código más mantenible, legible y flexible, que al fin y al cabo es lo que se intenta conseguir con este principio.

2. Open/Closed Principle (Principio de abierto/cerrado).

“Una entidad de software (clase, módulo, función, etc.) debe quedarse abierta para su extensión, pero cerrada para su modificación”.

Esto significa que debemos poder agregar nuevas funcionalidades sin modificar el código existente.

El objetivo es evitar hacer cambios directamente en el código ya escrito y, para eso, esos nuevos cambios se realizan a través de la extensión (por ejemplo, mediante la herencia).

Resumiendo, de lo que trata este principio es de evitar que modifiquemos el código existente para agregar nuevas funcionalidades.

Veamos el siguiente ejemplo:

class CalculadoraDescuentos:
    def calcular_descuentos(self, producto):
        if producto["tipo"] == "electronica":
            return producto["precio"] * 0.10 # 10% de descuento en electronica
        elif producto["tipo"] == "ropa":
            return producto["precio"] * 0.20 # 20% de descuento en ropa
        elif producto["tipo"] == "juguetes":
            return producto["precio"] * 0.15 # 15% de descuento en juguetes
        else:
            return 0 # Sin descuento

# Uso
productos = [
    {"nombre": "Laptop", "precio": 1000, "tipo": "electronica"},
    {"nombre": "Polo", "precio": 50, "tipo": "ropa"},
    {"nombre": "Muñeco", "precio": 20, "tipo": "juguetes"},
]

calculadora = CalculadoraDescuentos()
for producto in productos:
    descuento = calculadora.calcular_descuentos(producto)
    print(f"Descuento para {producto['nombre']}: {descuento}€")

Una vez más el ejemplo anterior seria incorrecto.

¿Por qué?

Sencillamente porque si queremos agregar un nuevo tipo de producto, tenemos que modificar directamente la clase CalculadoraDescuentos, por tanto, ya estaríamos violando el principio OCP.

Si cada vez que necesitamos agregar un nuevo producto o modificar un descuento modificamos dicha clase, esto, puede generar algún error, aparte de hacer el código más difícil de mantener.

Entonces… ¿Cuál sería el uso correcto?

Pues siguiendo con el ejemplo de descuentos…:

class Descuentos:
    def calcular_descuentos(self, producto):
        pass

# Descuento para productos electrónicos
class DescuentosElectronica(Descuentos):
    def calcular_descuentos(self, producto):
        return producto["precio"] * 0.10

# Descuento para ropa
class DescuentosRopa(Descuentos):
    def calcular_descuentos(self, producto):
        return producto["precio"] * 0.20

# Descuento para juguetes
class DescuentosJugetes(Descuentos):
    def calcular_descuentos(self, producto):
        return producto["precio"] * 0.15

# Clase para manejar los descuentos
class CalculadoraDescuentos:
    def __init__(self):
        self.estrategias = {}

    def anadir_descuento(self, tipo_producto, estrategia):
        self.estrategias[tipo_producto] = estrategia

    def calcular_descuentos(self, producto):
        estrategia = self.estrategias.get(producto["tipo"])
        if estrategia:
            return estrategia.calcular_descuentos(producto)
        return 0  # Sin descuento

calculadora = CalculadoraDescuentos()
calculadora.anadir_descuento("electronica", DescuentosElectronica())
calculadora.anadir_descuento("ropa", DescuentosRopa())
calculadora.anadir_descuento("juguetes", DescuentosJugetes())

productos = [
    {"nombre": "Laptop", "precio": 1000, "tipo": "electronica"},
    {"nombre": "Polo", "precio": 50, "tipo": "ropa"},
    {"nombre": "Muñeco", "precio": 20, "tipo": "juguetes"},
]

for producto in productos:
    descuento = calculadora.calcular_descuentos(producto)
    print(f"Descuento para {producto['nombre']}: {descuento}€")

En este último ejemplo, nuestra clase principal CalculadoraDescuentos, está cerrada para modificación, pero abierta para extensión y, haciendo uso de la ventaja del polimorfismo, agregamos nuevos tipos de descuento sin modificar el código existente.

De esta manera cumplimos el principio OCP, ya que, si necesitamos añadir un nuevo tipo de descuento, basta con crear una nueva clase implementando la interfaz Descuentos y luego la registramos en CalculadoraDescuentos.

Una vez más, haciéndolo de esta manera, nuestro código será más fácil de mantener y más flexible.

3. Liskov Substitution Principle (Principio de sustitución de Liskov)

“Cada clase que hereda de otra puede usarse como su padre sin necesidad de conocer las diferencias entre ellas.”

Dicho de forma más sencilla, si tenemos una clase B que es “hija” de la clase A, deberíamos poder usar B en cualquier lugar donde se use A, sin afectar al funcionamiento del programa.

La base de este principio es esa, que las clases derivadas deben ser capaces de heredar y utilizar los comportamientos y propiedades de las clases padre, sin alterar el correcto funcionamiento de nuestro programa.

Por ejemplo, imagina que tienes una clase llamada “Animal” que tiene un método hacer_sonido(). Pues bien, todos los animales puedan emitir sonidos, pero algunos, como los peces, no lo hacen de la misma manera…

Observa:

class Animal:
    def hacer_sonido(self):
        return "Cualquier sonido..."

class Perro(Animal):
    def hacer_sonido(self):
        return "Guau, guau!"

class Pez(Animal):
    def hacer_sonido(self):
        raise Exception("El pez no realiza ningún sonido")

def reproducir_sonido(animal):
    print(animal.hacer_sonido())

perro = Perro()
reproducir_sonido(perro)  # Guau, guau!

pez = Pez()
reproducir_sonido(pez)  # Error: El pez no realiza ningún sonido

El ejemplo anterior es incorrecto y no cumple el principio de sustitución de Liskov, dado que la clase Pez, no puede cumplir con el comportamiento que esperamos, es decir, hacer un sonido, y en su lugar, lanza una excepción rompiendo el principio LSP.

Ahora veamos cómo hacerlo correctamente:

class Animal:
    def hacer_sonido(self):
        pass

class Perro(Animal):
    def hacer_sonido(self):
        return "Guau, guau!"

class Pez(Animal):
    def hacer_sonido(self):
        return "Blub, blub"

def reproducir_sonido(animal):
    print(animal.hacer_sonido())

perro = Perro()
reproducir_sonido(perro)  # Guau, guau!

pez = Pez()
reproducir_sonido(pez)  # Blub, blub

En este caso, la aplicación del principio de sustitución de Liskov es correcto ya que todos los animales implementan el método hacer_sonido() de una manera lógica.

Por ejemplo los perros hacen “Guau, guau” mientras que los peces hacen «Blub, blub».

La función reproducir_sonido() ahora acepta cualquier tipo de Animal sin lanzar excepciones ni romper el comportamiento del programa que esperamos.

4. Interface segregation principle (Principio de segregación de la interfaz).

“Muchas interfaces cliente específicas son mejores que una interfaz de propósito general”.

Básicamente queremos decir que una clase A, por ejemplo, no puede ser forzada a implementar interfaces que no necesita y que no va a usar, es decir, se recomienda crear interfaces específicas para cada caso, en lugar de tener una interfaz grande y genérica.

Por ejemplo, imaginemos que tenemos un sistema de animales y, dependiendo del animal unos pueden andar, nadar y otros volar. Si definimos una interfaz general, estaríamos obligando a todos los animales a implementar métodos como nadar o volar que no necesitarían, por tanto, ya estaríamos violando el principio ISP, porque como digo, no todos los animales realizan las mismas acciones.

Mira el código siguiente:

class Acciones:
    def andar(self):
        pass

    def volar(self):
        pass

    def nadar(self):
        pass

# Un perro no vuela, pero estamos obligandolo a implementar el método...
class Perro(Acciones):
    def andar(self):
        return "El perro anda/camina"

    def volar(self):
        raise Exception("Un perro no puede volar! De momento...")

    def nadar(self):
        return "El perro nada"

# Obligamos a 'Pez' a implementar el método andar y volar...
class Pez(Acciones):
    def andar(self):
        return "Un pez no puede andar..."

    def volar(self):
        raise Exception("Por el momento, un pez no puede volar...")

    def nadar(self):
        return "El pez nada"

perro = Perro()
print(perro.andar()) # "El perro anda/camina"
print(perro.volar()) # Error: Un perro no puede volar! De momento...

El ejemplo anterior es incorrecto y supone una violación del principio porque estamos obligando a todos los animales a implementar métodos que no necesitan.

Ahora fíjate en el siguiente código:

class Andar:
    def andar(self):
        pass

class Nadar:
    def nadar(self):
        pass

class Volar:
    def volar(self):
        pass

# El perro puede andar y nadar
class Perro(Andar, Nadar):
    def andar(self):
        return "El perro anda"

    def nadar(self):
        return "El perro nada"

# El pez solo puede nadar
class Pez(Nadar):
    def nadar(self):
        return "El pez nada..."

# El pájaro vuela...
class Pajaro(Volar):
    def volar(self):
        return "El pájaro vuela..."

# Uso correcto
perro = Perro()
print(perro.andar()) # "El perro anda"
print(perro.nadar()) # "El perro nada"

pez = Pez()
print(pez.nadar()) # "El pez nada..."

pajaro = Pajaro()
print(pajaro.volar()) # "El pájaro vuela..."

La diferencia es bastante evidente. Dividimos la interfaz general en interfaces más pequeñas y específicas y de esa manera, cada clase animal solo implementa las interfaces que necesita sin necesidad de implementar métodos innecesarios.

De esta forma, ningún animal tiene que implementar métodos innecesarios o irrelevantes para ellos, cumpliendo con el principio de segregación de interfaces (ISP).

Una vez más, con esto conseguimos un diseño más fácil de mantener y flexible.

5. Dependency inversion principle (Principio de inversión de la dependencia).

“Los módulos de alto nivel no deben depender de los módulos de bajo nivel. Ambos deben depender de abstracciones.

Las abstracciones no deben depender de los detalles. Los detalles (implementaciones concretas) deben depender de las abstracciones.”

«Depender de abstracciones, no depender de implementaciones».

Básicamente quiere decir que evitemos la dependencia directa entre clases y, en su lugar, dependamos de interfaces o abstracciones.

Imagina que tienes un sistema de notificaciones para enviar mensajes vía email y SMS.

class Email:
    def enviar_email(self, mensaje):
        return f"Enviando email: {mensaje}"

class SMS:
    def enviar_sms(self, mensaje):
        return f"Enviando SMS: {mensaje}"

class Notificacion:
    def __init__(self):
        self.email = Email() # Dependencia directa de la clase Email
        self.sms = SMS() # Dependencia directa de la clase SMS

    def enviar(self, mensaje):
        resultado = self.email.enviar_email(mensaje)
        resultado2 = self.sms.enviar_sms(mensaje)
        return f"{resultado}\n{resultado2}"

notificacion = Notificacion()
print(notificacion.enviar("Hola, que tal?"))

Este sistema estaría implementado incorrectamente debido a que la clase Notificacion, depende directamente de las clases Email y SMS. Si queremos añadir un nuevo sistema de notificación tenemos que modificar la clase Notificacion… ¿Qué quiero decir?

Qué la clase de alto nivel Notificacion, esta fuertemente acoplada a las implementaciones de bajo nivel (que son la clase Email y la clase SMS).

Recuerda lo que dice el principio: “Depender de abstracciones, no depender de implementaciones”. Por tanto, en este ejemplo, ya lo estaríamos violando.

Para hacerlo correctamente deberíamos implementar abstracciones a través de interfaces.

Por ejemplo:

class Mensaje:
    def enviar(self, mensaje):
        pass

class Email(Mensaje):
    def enviar(self, mensaje):
        return f"Enviando email: {mensaje}"

class SMS(Mensaje):
    def enviar(self, mensaje):
        return f"Enviando SMS: {mensaje}"

class Notificacion:
    def __init__(self, servicios):
        self.servicios = servicios # Inyectamos servicios como dependencias

    def enviar(self, mensaje):
        resultados = []
        for servicio in self.servicios:
            resultados.append(servicio.enviar(mensaje))
        return "\n".join(resultados)

servicio = [Email(), SMS()]
notificacion = Notificacion(servicio)
print(notificacion.enviar("Hola, que tal?"))

Ahora estaríamos implementando el principio de inversión de dependencias correctamente, dado que la clase Notificacion no depende de clases concretas, sino de abstracciones. En este caso de la abstracción Mensaje.

Si queremos agregar nuevos servicios de notificación, no es necesario modificar la clase de alto nivel (Notificacion). Simplemente creamos una nueva clase que implemente Mensaje y punto.

Haciéndolo de esta manera, aplicamos correctamente el principio de inversión de dependencias y nuestro código vuelve a ser más fácil de mantener y flexible.

Y hasta aquí todos los principios SOLID. Los ejemplos son bastante claros y los principios en si son fáciles de aplicar, pero también son fáciles de romper, por eso es esencial conocerlos bien para poder crear buen código.

Sobre el autor

Comparte:

Deja una respuesta

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

Información básica sobre protección de datos
Responsable Óscar Martínez +info...
Finalidad Gestionar y moderar los comentarios +info...
Legitimación Consentimiento del interesado. +info...
Destinatarios No se cederán datos a terceros, salvo obligación legal +info...
Derechos Acceder, rectificar y cancelar los datos, así como otros derechos. +info...
Información adicional Puedes consultar la información adicional y detallada sobre protección de datos en nuestra página de política de privacidad.

Este sitio esta protegido por reCAPTCHA y laPolítica de privacidady losTérminos del servicio de Googlese aplican.

El periodo de verificación de reCAPTCHA ha caducado. Por favor, recarga la página.