Principios SOLID

Principios SOLID

Los principios SOLID son un conjunto de cinco directrices para el diseño de software orientado a objetos (POO) que promueven la creación de código más limpio, mantenible y escalable. El acrónimo SOLID proviene de las primeras letras de cada principio:

S - Principio de responsabilidad única (Single Responsibility Principle - SRP)

Este principio establece que cada clase o módulo debe tener una única responsabilidad bien definida. Una clase que cumple con el SRP es más fácil de entender, modificar y probar.

Ejemplo.

Escenario sin SRP:

Imagina una clase llamada Empleado en un sistema de nóminas. Esta clase tiene varias responsabilidades, como:

  • Almacenar información del empleado (nombre, dirección, etc.).
  • Calcular el salario del empleado.
  • Generar recibos de pago.
  • Enviar notificaciones por correo electrónico al empleado sobre su salario y recibos.

Esta clase viola el principio SRP porque tiene demasiadas responsabilidades. Si se necesita cambiar la forma en que se calcula el salario del empleado, por ejemplo, se tendría que modificar la clase Empleado. Esto podría tener efectos secundarios no deseados en otras partes del sistema que dependen de la clase Empleado para generar recibos de pago o enviar notificaciones por correo electrónico.



Escenario con SRP:

Para seguir el principio SRP, se pueden dividir las responsabilidades de la clase Empleado en clases separadas:

  • Clase Empleado: Esta clase solo almacenaría la información del empleado.
  • Clase CalculadoraSalario: Esta clase se encargaría de calcular el salario del empleado.
  • Clase GeneradorRecibos: Esta clase se encargaría de generar recibos de pago para el empleado.
  • Clase NotificadorCorreoElectronico: Esta clase se encargaría de enviar notificaciones por correo electrónico al empleado sobre su salario y recibos.


Al dividir las responsabilidades de esta manera, cada clase tiene una única responsabilidad bien definida. Esto hace que el código sea más fácil de entender, modificar y probar.

Beneficios de aplicar SRP en este caso:

  • Mayor claridad: Las responsabilidades de cada clase están bien definidas, lo que hace que el código sea más fácil de entender.
  • Mayor facilidad de mantenimiento: Si se necesita cambiar la forma en que se calcula el salario del empleado, solo se tendría que modificar la clase CalculadoraSalario. Esto no afectaría a las otras clases.
  • Mayor reusabilidad: Las clases CalculadoraSalario, GeneradorRecibos y NotificadorCorreoElectronico podrían usarse en otros sistemas que necesitan realizar tareas similares.
  • Mayor testabilidad: Cada clase es más pequeña y tiene una única responsabilidad, lo que la hace más fácil de probar.

En resumen, el principio SRP es una herramienta valiosa para crear código limpio, mantenible y reutilizable. Al seguir este principio, se pueden diseñar sistemas de software más robustos y escalables.

O - Principio abierto/cerrado (Open/Closed Principle - OCP)

El principio abierto/cerrado indica que las clases deben estar abiertas para su extensión, pero cerradas para su modificación. Esto significa que se debe poder agregar nueva funcionalidad a una clase sin tener que modificar su código existente.

Ejemplo.

Escenario sin OCP:

Imagina una clase Forma que se utiliza para dibujar diferentes formas geométricas, como cuadrados, círculos y triángulos. La clase Forma tiene un método dibujar que dibuja la forma en la pantalla.

Para agregar una nueva forma, como un pentágono, se tendría que modificar la clase Forma. Esto viola el principio OCP porque el código existente se está modificando para agregar nueva funcionalidad. Si se modifican las clases existentes para agregar nuevas funcionalidades, el código se vuelve más frágil y difícil de mantener.



Escenario con OCP:

Para seguir el principio OCP, se puede crear una interfaz Forma que defina el método dibujar. Las clases que implementan la interfaz Forma (como Cuadrado, Circulo, Triangulo y Pentagono) son responsables de dibujar su propia forma en la pantalla.

La clase Forma no necesita modificarse para agregar nuevas formas. Simplemente se crea una nueva clase que implemente la interfaz Forma y que dibuje la nueva forma.


Beneficios de aplicar OCP en este caso:

  • Mayor flexibilidad: Se pueden agregar nuevas formas sin modificar el código existente.
  • Mayor facilidad de mantenimiento: El código es más fácil de mantener porque las clases existentes no se modifican cuando se agregan nuevas funcionalidades.
  • Mayor reusabilidad: La interfaz Forma se puede reutilizar para crear diferentes tipos de formas.
  • Mayor testabilidad: Las clases que implementan la interfaz Forma son más fáciles de probar porque se pueden probar de forma independiente.

En resumen, el principio OCP es una herramienta valiosa para crear código flexible, mantenible y reutilizable. Al seguir este principio, se pueden diseñar sistemas de software más robustos y escalables.

Otro ejemplo.

Imagina una biblioteca de autenticación que proporciona clases para autenticar usuarios utilizando diferentes métodos, como contraseñas, tokens de autenticación y autenticación biométrica.

Si la biblioteca se diseñara sin seguir el principio OCP, cada vez que se agregue un nuevo método de autenticación, se tendría que modificar la biblioteca. Esto podría tener efectos secundarios no deseados en otras partes del código que dependen de la biblioteca de autenticación.

Para seguir el principio OCP, se puede crear una interfaz Autenticacion que defina el método autenticar. Las clases que implementan la interfaz Autenticacion (como AutenticacionContrasena, AutenticacionToken y AutenticacionBiometrica) son responsables de autenticar al usuario utilizando su método respectivo.

Con este diseño, se pueden agregar nuevos métodos de autenticación sin modificar el código existente. Simplemente se crea una nueva clase que implemente la interfaz Autenticacion y que autentique al usuario utilizando el nuevo método.

L - Principio de sustitución de Liskov (Liskov Substitution Principle - LSP)

El principio de sustitución de Liskov establece que los subtipos deben ser sustituibles por sus tipos base sin alterar el comportamiento correcto de los programas que dependen de ellos. En otras palabras, si una clase hereda de otra, debería poder usarse en cualquier lugar donde se use la clase base sin causar problemas.

Ejemplo del Principio de Sustitución de Liskov (LSP)

Escenario sin LSP:

Imagina una jerarquía de clases que representa diferentes tipos de rectángulos:

  • Clase Rectangulo: Esta clase representa un rectángulo general con ancho y alto.
  • Clase Cuadrado: Esta clase hereda de la clase Rectangulo y representa un cuadrado con lados iguales.

Supongamos que tenemos una función que calcula el área de un rectángulo:

Python
def calcular_area(rectangulo):
  return rectangulo.ancho * rectangulo.alto

Esta función funciona correctamente con objetos de la clase Rectangulo y Cuadrado. Sin embargo, si intentamos usar la función con un objeto de una clase que viola el principio LSP, como una clase RectanguloElastico que puede cambiar su ancho y alto, el resultado podría ser incorrecto.

Ejemplo de clase RectanguloElastico que viola LSP:

Python
class RectanguloElastico(Rectangulo):
  def __init__(self, ancho, alto):
    super().__init__(ancho, alto)

  def estirar(self, factor_ancho, factor_alto):
    self.ancho *= factor_ancho
    self.alto *= factor_alto

Si usamos un objeto RectanguloElastico con la función calcular_area después de estirarlo, el resultado será incorrecto:

Python
rectangulo_elastico = RectanguloElastico(5, 3)
calcular_area(rectangulo_elastico)  # Resultado: 15

rectangulo_elastico.estirar(2, 3)
calcular_area(rectangulo_elastico)  # Resultado: 45 (Incorrecto, debería ser 90)

Escenario con LSP:

Para que la jerarquía de clases cumpla con el principio LSP, la clase RectanguloElastico debe sobrescribir el método calcular_area para que devuelva el área correcta después de estirarse:

Python
class RectanguloElastico(Rectangulo):
  def __init__(self, ancho, alto):
    super().__init__(ancho, alto)

  def estirar(self, factor_ancho, factor_alto):
    super().estirar(factor_ancho, factor_alto)

  def calcular_area(self):
    return self.ancho * self.alto

Con esta modificación, la función calcular_area siempre devolverá el área correcta, independientemente de si se usa con un objeto de la clase Rectangulo, Cuadrado o RectanguloElastico.

Beneficios de aplicar LSP en este caso:

  • Mayor previsibilidad: El código se comporta de manera predecible, incluso cuando se usan subtipos en lugar de tipos base.
  • Mayor facilidad de depuración: Si hay errores en el código, es más fácil encontrarlos porque el comportamiento de los subtipos es coherente con el de los tipos base.
  • Mayor robustez: El código es más robusto porque es menos probable que se rompa cuando se usan subtipos en lugar de tipos base.

En resumen, el principio LSP es una herramienta valiosa para crear código predecible, fácil de depurar y robusto. Al seguir este principio, se pueden diseñar sistemas de software más confiables y mantenibles.

I - Principio de segregación de interfaces (Interface Segregation Principle - ISP)

El principio de segregación de interfaces indica que se deben evitar las interfaces con muchos métodos específicos y, en su lugar, se deben crear interfaces más pequeñas y específicas para cada grupo de clientes relacionados. Esto hace que las interfaces sean más fáciles de usar y menos propensas a cambios.

Ejemplo del Principio de Segregación de Interfaces (ISP)

Escenario sin ISP:

Imagina una interfaz ImpresoraMultifuncion que define los siguientes métodos:

  • imprimir(): Imprime un documento.
  • escanear(): Escanea un documento.
  • fotocopiar(): Hace una fotocopia de un documento.
  • enviarFax(): Envía un fax.


Esta interfaz viola el principio ISP porque agrupa métodos que no son relevantes para todos los usuarios. Por ejemplo, un usuario que solo necesita imprimir documentos no necesita tener acceso a los métodos escanear(), fotocopiar() y enviarFax().

Escenario con ISP:

Para seguir el principio ISP, se pueden crear interfaces más específicas para cada grupo de funcionalidades:

  • Interfaz Impresora: Define el método imprimir().
  • Interfaz Escaner: Define el método escanear().
  • Interfaz Fotocopiadora: Define el método fotocopiar().
  • Interfaz Fax: Define el método enviarFax().

Las clases que implementan estas interfaces solo deben incluir los métodos que necesitan. Por ejemplo, una clase ImpresoraLaser solo implementaría la interfaz Impresora, mientras que una clase ImpresoraMultifuncionAvanzada implementaría las interfaces Impresora, Escaner, Fotocopiadora y Fax.



Beneficios de aplicar ISP en este caso:

  • Mayor flexibilidad: Los usuarios solo tienen que usar las interfaces que necesitan.
  • Mayor facilidad de uso: Las interfaces son más fáciles de usar porque solo contienen métodos relevantes.
  • Mayor reusabilidad: Las interfaces se pueden reutilizar para crear diferentes tipos de dispositivos multifunción.
  • Mayor testabilidad: Las interfaces son más fáciles de probar porque solo contienen métodos relevantes.

Ejemplo adicional:

Imagina una biblioteca de clases para dibujar diferentes formas geométricas. Si la biblioteca se diseñara sin seguir el principio ISP, se tendría una única interfaz Forma que definiría métodos para dibujar, redimensionar, rotar y colorear todas las formas.

Esto viola el principio ISP porque no todos los métodos son relevantes para todas las formas. Por ejemplo, un círculo no necesita tener un método para rotar, y un cuadrado no necesita tener un método para cambiar su forma.

Para seguir el principio ISP, se pueden crear interfaces más específicas para cada tipo de forma, como:

  • Interfaz FormaGeometrica: Define los métodos para dibujar y redimensionar todas las formas.
  • Interfaz FormaCerrada: Define el método para colorear todas las formas cerradas (como círculos y cuadrados).
  • Interfaz FormaRotable: Define el método para rotar todas las formas que se pueden rotar (como rectángulos y triángulos).

Las clases que implementan estas interfaces solo deben incluir los métodos que necesitan. Por ejemplo, una clase Circulo implementaría las interfaces FormaGeometrica y FormaCerrada, mientras que una clase Rectangulo implementaría las interfaces FormaGeometrica y FormaRotable.

En resumen, el principio ISP es una herramienta valiosa para crear interfaces flexibles, fáciles de usar, reutilizables y fáciles de probar. Al seguir este principio, se pueden diseñar sistemas de software más modulares y mantenibles.

D - Principio de inversión de dependencias (Dependency Inversion Principle - DIP)

El principio de inversión de dependencias establece que las clases de alto nivel no deben depender de clases de bajo nivel; ambas deben depender de abstracciones. Las abstracciones pueden ser interfaces o clases abstractas. Al seguir este principio, el código se vuelve más desacoplado y más fácil de probar.

Los principios SOLID son pautas generales que no siempre se pueden aplicar de forma estricta en todas las situaciones. Sin embargo, son una herramienta valiosa para los desarrolladores de software que desean crear código más limpio, mantenible y escalable.

Ejemplo del Principio de Inversión de Dependencias (DIP)

Escenario sin DIP:

Imagina una clase CalculadoraSalario que calcula el salario de un empleado. La clase CalculadoraSalario depende directamente de la clase Empleado para obtener la información del empleado necesaria para calcular el salario.

Esta dependencia hace que el código sea difícil de probar porque es difícil crear objetos ficticios (mocks) de la clase Empleado para usar en las pruebas unitarias de la clase CalculadoraSalario.

Escenario con DIP:

Para seguir el principio DIP, se puede introducir una interfaz IProveedorInformacionEmpleado que defina los métodos para obtener la información del empleado necesaria para calcular el salario. La clase CalculadoraSalario no debe depender directamente de la clase Empleado, sino que debe depender de la interfaz IProveedorInformacionEmpleado.

De esta manera, la clase CalculadoraSalario se puede usar con cualquier clase que implemente la interfaz IProveedorInformacionEmpleado, incluida una clase ficticia (mock) que se puede usar en las pruebas unitarias.

Ejemplo de implementación del DIP:

Python
from abc import ABC, abstractmethod

class IProveedorInformacionEmpleado(ABC):
  @abstractmethod
  def obtener_nombre(self) -> str:
    pass

  @abstractmethod
  def obtener_cargo(self) -> str:
    pass

  @abstractmethod
  def obtener_salario_base(self) -> float:
    pass

class Empleado(IProveedorInformacionEmpleado):
  def __init__(self, nombre, cargo, salario_base):
    self.nombre = nombre
    self.cargo = cargo
    self.salario_base = salario_base

  def obtener_nombre(self) -> str:
    return self.nombre

  def obtener_cargo(self) -> str:
    return self.cargo

  def obtener_salario_base(self) -> float:
    return self.salario_base

class CalculadoraSalario:
  def __init__(self, proveedor_informacion_empleado: IProveedorInformacionEmpleado):
    self.proveedor_informacion_empleado = proveedor_informacion_empleado

  def calcular_salario(self) -> float:
    nombre = self.proveedor_informacion_empleado.obtener_nombre()
    cargo = self.proveedor_informacion_empleado.obtener_cargo()
    salario_base = self.proveedor_informacion_empleado.obtener_salario_base()

    # Aplicar reglas de negocio para calcular el salario
    # ...

    return salario_base

# Ejemplo de uso con una clase Empleado real
empleado = Empleado("Juan Perez", "Desarrollador", 1500.0)
calculadora_salario = CalculadoraSalario(empleado)
salario = calculadora_salario.calcular_salario()
print(f"El salario de {empleado.nombre} es de {salario}")

# Ejemplo de uso con una clase mock para pruebas unitarias
class MockProveedorInformacionEmpleado(IProveedorInformacionEmpleado):
  def obtener_nombre(self) -> str:
    return "Juan Perez"

  def obtener_cargo(self) -> str:
    return "Desarrollador"

  def obtener_salario_base(self) -> float:
    return 1500.0

mock_proveedor_informacion_empleado = MockProveedorInformacionEmpleado()
calculadora_salario_mock = CalculadoraSalario(mock_proveedor_informacion_empleado)
salario_mock = calculadora_salario_mock.calcular_salario()
assert salario_mock == 1500.0  # Prueba unitaria exitosa

Beneficios de aplicar DIP en este caso:

  • Mayor testabilidad: El código es más fácil de probar porque se pueden usar mocks para las dependencias.
  • Mayor flexibilidad: El código es más flexible porque se puede usar con diferentes implementaciones de la interfaz IProveedorInformacionEmpleado.
  • Mayor reusabilidad: Las clases se pueden reutilizar en diferentes contextos porque no dependen de clases específicas.
  • Mayor mantenibilidad: El código es más fácil de mantener porque los cambios en una clase no afectan a otras clases que dependen de ella.

En resumen, el principio DIP es una herramienta valiosa para crear código testable, flexible, reutilizable y mantenible. Al seguir este principio, se pueden diseñar sistemas de software más robustos y escalables.

Beneficios de seguir los principios SOLID:

  • Código más limpio y fácil de entender: Al seguir los principios SOLID, las clases y módulos son más pequeños y tienen responsabilidades bien definidas. Esto hace que el código sea más fácil de leer y comprender, lo que facilita su mantenimiento y modificación.
  • Código más mantenible: El código que cumple con los principios SOLID es más fácil de mantener porque es menos probable que se rompa cuando se realizan cambios. Esto se debe a que los cambios en una clase generalmente no afectan a otras clases.
  • Código más escalable: El código que sigue los principios SOLID es más escalable porque es más fácil agregar nuevas funcionalidades sin tener que reescribir el código existente. Esto se debe a que las clases están abiertas para su extensión pero cerradas para su modificación.

Resumen

En resumen, los principios SOLID son una herramienta valiosa para los desarrolladores de software que desean crear código de alta calidad. Al seguir estos principios, se puede crear código que sea más limpio, mantenible y escalable.

Comentarios

Entradas más populares de este blog

Descomposición arquitectónica de software

Gráficas de Pareto