Software Crafters ® | Creado con 🖤 para elevar el nivel de la conversación sobre programación en español| Legal
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í:
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.
[bctt tweet="Nunca debería haber más de un motivo por el cual cambiar una clase"]
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.
"Todas las entidades software deberían estar abiertas a extensión, pero cerradas a modificación"
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)
"Las funciones que utilicen punteros o referencias a clases base deben ser capaces de usar objetos de clases derivadas sin saberlo." username="unclebobmartin"
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.
"Los clientes no deberían estar obligados a depender de interfaces que no utilicen."
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.
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.
"Los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deben depender de abstracciones. "
"Las abstracciones no deben depender de concreciones. Los detalles deben depender de abstracciones."
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í.
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)