aprende python

Principios SOLID en Python

El objetivo de este artículo es que el lector aprenda aplicar los principios SOLID con el lenguaje Python. SOLID es un acrónimo creado por Michael Feathers para los principios publicados por Robert C. Martin, en su libro Agile Software Development: Principles, Patterns, and Practices.

Se trata de cinco principios de diseño orientado a objetos que nos ayudarán a crear mejor código, más estructurado, con clases de responsabilidad más definida y más desacopladas entre sí:

  • Single Responsibility: Responsabilidad única.
  • Open/Closed: Abierto/Cerrado.
  • Liskov substitution: Sustitución de Liskov.
  • Interface segregation: Segregación de interfaz.
  • Dependency Inversion: Inversión de dependencia.

Es importante resaltar que se trata de principios, no de reglas. Una regla es de obligatorio cumplimiento, en cambio, los principios son recomendaciones que pueden ayudar a hacer las cosas mejor. Además, siempre puedes encontrar algún contexto en el que te los puedas saltar, lo importante es hacerlo de forma consciente.

Single Responsibility Principle (SRP)

Nunca debería haber más de un motivo por el cual cambiar una clase Click Para Twittear

El primero de los cinco principios, single responsibility principle o en castellano, principio de responsabilidad única, viene a decir que una clase debe tener tan solo una única responsabilidad. A finales de los 80, Kent Beck y Ward Cunningham, ya aplicaban este principio mediante tarjetas CRC (Class, Responsibility, Collaboration) con las que detectaban responsabilidades y colaboraciones entre clases.

El principio responsabilidad única no se basa en diseñar clases con un sólo método, sino éstas tan sólo deberían tener una fuente de cambio. En otras palabras, aquellas clases que se vieran obligadas a cambiar ante una modificación en la base de datos y a la vez ante un cambio en la lógica de negocio, tendría más de un motivo para cambiar, es decir, más de una responsabilidad.

Este principio se suele incumplir  cuando en una misma clase están involucradas varias capas de la arquitectura. Veamos un ejemplo:

class Vehicle(object):
    def __init__(self, name):
        self._name = name
	self._persistence = MySQLdb.connect()
        self._engine = Engine()

    def getName():
        return self._name()

    def getEngineRPM():
        return self._engine.getRPM()

    def getMaxSpeed():
        return self._speed

    def print():
        return print ‘Vehicle: {}, Max Speed: {}, RMP: {}’.format(self._name, self._speed, self._engine.getRPM())

A primera vista se puede detectar que estamos mezclando tres capas muy diferenciadas: la lógica de negocio,  la lógica de presentación y la lógica de persistencia. Además estamos instanciando la clase engine dentro de vehicle, así que también nos estamos saltando el principio de inversión de dependencias.

Una solución para el problema de las múltiples responsabilidades de la clase anterior, podría pasar por abstraer dos clases, como por ejemplo VehicleRepository para manejar la persistencia y VehiclePrinter para gestionar la capa de presentación.

class Vehicle(object):
    def __init__(self, name, engine):
        self._name = name
        self._engine = engine

    def getName(self):
        return self._name()

    def getEngineRPM(self):
        return self._engine.getRPM()

    def getMaxSpeed(self):
        return self._speed


class VehicleRepository(object):
    def __init__(self, vehicle, db)
        self._persistence = db
        self._vehicle = vehicle


class VehiclePrinter(object):
    def __init__(self, vehicle, db)
        self._persistence = db
        self._vehicle = vehicle
    
    def print(self):
        return print ‘Vehicle: {}, Max Speed: {}, RMP: {}’.format(self._vehicle.getName(), self._vehicle.getMaxSpeed(), self._vehicle.getRPM())

En este caso se veía muy claro lo que teníamos que hacer para aplicar correctamente el SRP, pero muchas veces los detalles serán más sutiles y probablemente no los detectarás a la primera. No te obseciones simplemente aplica el sentido común.

Open-Closed Principle (OCP)

Todas las entidades software deberían estar abiertas a extensión, pero cerradas a modificación Click Para Twittear

El segundo de los principios,  Open-Closed (Abierto/Cerrado), cuyo nombre se lo debemos a Bertrand Meyer. Básicamente nos recomienda que cuando se pretende introducir un nuevo comportamiento en un sistema existente, en lugar de modificar las clases antiguas, se deben crear nuevas mediante herencia y redefinición de los métodos de la clase padre (polimorfismo), o inyectando dependencias que implementen el mismo contrato.

Este principio promete mejoras en la estabilidad de tu aplicación al evitar que los objetos existentes cambien con frecuencia, lo que también hace que las cadenas de dependencia sean un poco menos frágiles ya que habría menos partes móviles de las que preocuparse. Este principio aplica bien a la hora trabajar con un framework o con código legacy, evidentemente si el código lo has hecho tu o tu equipo, refactoriza.

Un buen ejemplo en el que se aplica este principio es el que veíamos en este artículo a la hora de extender el user de django desde la clase AbstractUser

from django.db import models

from django.db import models
from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
    bio = models.TextField(max_length=500, blank=True)
    location = models.CharField(max_length=30, blank=True)
    birth_date = models.DateField(null=True, blank=True)

Liskov Substitution Principle (LSP)

Las funciones que utilicen punteros o referencias a clases base deben ser capaces de usar… Click Para Twittear

El principio de Sustitución de Liskov, obtiene su nombre de Barbara Liskov. Este principio está relacionado con el anterior en lo que a la extensibilidad de las clases se refiere, y viene a decir que dada una instancia de una clase B, siendo esta un subtipo de una clase A, debemos poder sustituirla por una instancia de la clase A sin mayor problema.

En los lenguajes orientados a objetos de tipado estático, este principio describe principalmente una regla sobre una relación entre una subclase y una superclase. Cuando hablamos de lenguajes de tipado dinámico como Python, nos interesa qué mensajes responde ese objeto en lugar de a qué clase pertenece.

Un ejemplo que ilustra bastante bien la importancia de este principio es el de tratar de modelar un cuadrado como la concreción de un rectángulo:

class Rectangle(object):

    def getWidth(self)
        return _width
 
    def setWidth(self, width) 
        self._width = width
    
    def getHeight(self)
        return _height
 
    def setHeight(self, height) 
        self._height = height
 
    def calculateArea(self) 
        return self._width * self._height;
 
class Square(Rectangle):
    def setWidth(self, width) 
        self._width = width
        self._height = height

    def setWidth(self, height) 
        self._height = height
        self._width = width

class TestRectangle(unittest.TestCase):
 
    def setUp(self):
        pass
 
    def test_calculateArea(self):
        r = Rectangle()
        r.setWidth(5);
        r.setHeight(4);
        self.assertEqual(r.calculateArea(), 20)

Si tratamos de sustituir en el test el rectángulo por un cuadrado, el test no se cumple, ya que el resultado sería 16 en lugar de 20. Estaríamos por tanto violando el principio de sustitución de Liskov.

La regla principal para no violar este principio es básicamente tratar de heredar lo menos posible o  no usar los mixins a menos que se esté bastante seguro de que el comportamiento que está implementando no interferirá con las operaciones internas de sus ancestros.

Interface Segregation Principle (ISP)

Los clientes no deberían estar obligados a depender de interfaces que no utilicen. Click Para Twittear

El principio de segregación de la interfaz nos indica que ninguna clase debería depender de métodos que no usa. Cuando creemos interfaces (clases en lenguajes interpretados como Python) que definan comportamientos, es importante estar seguros de que todas los objetos que implementen esas interfaces/clases se vayan a necesitar, de lo contrario, es mejor tener varias interfaces/clases pequeñas.

Este principio está íntimamente relacionado al de responsabilidad única, ya que cuando usamos SRP estamos a la vez aplicando este principio, podría decirse que el principio de segregación de la interfaz nos ayuda a mantener el principio de responsabilidad única. Cuando se da el caso de que una clase o interfaz tiene más métodos de los que una necesita para sí mismo, es muy probable que sirva a objetos con responsabilidades diferentes.

Una forma de no violar este principio en Python es aplicando duck typing. Este concepto viene decir que los métodos y propiedades de un objeto determinan su validez semántica, en vez de su jerarquía de clases o la implementación de una interfaz específica.

Dependency Inversión Principle (DIP)

Los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deben depender de abstracciones. Click Para Twittear Las abstracciones no deben depender de detalles. Los detalles deben depender de abstracciones. Click Para Twittear

Quinto y último de los principios, la inversión de dependencia, cuyo objetivo principal es desacoplar nuestro código de sus dependencias directas.  Este principio viene a decir que las clases de las capas superiores no deberían depender de las clases de las capas inferiores, sino que ambas deberían depender de abstracciones. A su vez, dichas abstracciones no deberían depender de los detalles, sino que son los detalles los que deberían depender de las mismas.

La inversión de dependencias da origen a la inyección de dependencias. Este concepto se basa en hacer que una clase A inyecte objetos en una clase B en lugar de dejar que sea la propia clase B la que se encargue de instanciar el objeto. Veamoslo con el ejemplo del vehiculo:

class Engine(object):
    def __init__(self):
        pass
      
    def accelerate(self):
        pass
 
    def getRPM(self):
        currentRPM = 0
        #...
        return currentRPM
 
class Vehicle(object):
    def __init__(self):
        self._engine = Engine()
        
    def getEngineRPM(self)
        return self._engine.getRPM();

El código anterior ilustra la manera “habitual” de definir la colaboración entre clases. Como podemos observar, existe una clase Vehicle que contiene un objeto de la clase Engine. La clase Vehicle obtiene las revoluciones del motor invocando el método getEngineRPM del objeto Motor y devolviendo su resultado. Este caso se corresponde con una dependencia, el módulo superior Vehicle depende del módulo inferior Engine, lo cual genera un código tremendamente acoplado y dificil de testear.

Para desacoplar la dependencia Engine de Vehicle debemos hacer que la clase Vehicle deje de responsabilizarse de instanciar el objeto Engine, inyectándolo como parámetro al constructor, evitando así que la responsabilidad recaiga sobre la propia clase. De este modo desacoplamos ambos objetos, quedando la clase tal que así:

class Vehicle(object):
    def __init__(self, engine):
        self._engine = engine
        
    def getEngineRPM(self)
        return self._engine.getRPM();

Ahora la responsabilidad de instanciar la clase engine ya no corresponde a la clase vehicule. Además, en Python, el parámetro engine no tiene porqué ser una instancia de la clase engine, podría ser cualquier objeto siempre y cuando tuviera un método getRPM(). Esta es una ventaja inherente a los lenguajes dinámicos, ya que nos permiten aprovechar el duck typing y evitar así tener que definir el tipo de la dependencia.

Hasta ahora no he comentado nada de sobre los contenedores de inversión de control, aunque no es necesario para hacer inyección de dependencias, puede ser interesante su uso, sobre todo en los lenguajes de tipado estático. En los lenguajes dinámicos los contenedores de inversión de control pierden su interés ya que en los constructores de las clases no está especificado el tipo de las dependencias y si quieren usar estarás obligado a definir de forma un tanto forzada las dependencias entre los objetos para que el contenedor pueda componer unos con otros.

Como hemos podido ver, la inyección de dependencias por si misma nos ayuda a crear clases con responsabilidad más definida, más estructuradas y desacopladas entre sí.

Resumen

Los prinpicios SOLID, pese al abuso que se hace últimamente de ellos, son una herramienta que nos ayudan a comprender mejor el diseño de software orientado a objetos. Si los aplicas con sentido común, sin dogmatizarlos, estarás en mejores condiciones para encontrar optimas soluciones a los problemas software.

Si te ha gustado el artículo, valora y comparte en tus redes sociales. No dudes en comentar dudas, aportes o sugerencias, estaré encantado de responder.
Este artículo se distribuye bajo una Licencia Creative Commons Reconocimiento-CompartirIgual 4.0 Internacional (CC BY-SA 4.0)

licencia-cc

Principios SOLID en Python
5 (100%) 15 votos
5 comentarios
  1. Matías
    Matías Dice:

    Justamente hace poco estuve hablando de esto. Antes que nada aclaro que estoy terminando carrera sistemas y no tuve oportunidad de programar en python.
    Ahora si, mientras estudiaba principios solid llegamos a concluir que como python no dispone de interfaces, no se podrían aplicar, ya que muchos principios residen en abstraccion de interfaces para aplicarlos, como es el caso del polimorfismo que permite cambiar comportamiento en tiempo de ejecución.
    En definitiva, leyendo principios del presente articulo los comprendí, pero el caso puntual de segregación de interfaces no quedo claro. Quizás si se expandiera como sería utilizar clases como interfaces, al ser python un lenguaje interpretado.
    Muy interesante el post, saludos!

    Responder
    • Miguel A. Gómez
      Miguel A. Gómez Dice:

      Hola Matias antes que nada gracias por tu comentario.

      Como sabrás una interfaz es un elemento de OO que contiene las definiciones de un grupo de funcionalidades relacionadas que una clase puede implementar, básicamente vienen a ser como las clases abstractas.

      Son interesantes en los lenguajes de programación que no admiten herencia múltiple, pero no tiene sentido en lenguajes que admiten multiherencia como Python, por lo tanto para simular las interfaces en Python simplemente bastaría con definir los comportamientos en clases abstractas.

      Ahora bien, ¿esto tiene sentido en Python?, no demasiado, ya que si usas Python lo suyo es aprovechar las ventajas del Duck typing. Es por ello, que el principio de segregación de la interfaz pierde sentido en los lenguajes de tipado dinámico, de ahí que no haya hecho demasiado incapié en el tema.

      Saludos!

      Responder

Dejar un comentario

¿Quieres unirte a la conversación?
Siéntete libre de contribuir

Deja un comentario

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