We structure our software to make change easier; after all, software is meant to be soft.
Los principios S.O.L.I.D. son cinco. A saber:
- Principio de responsabilidad única (Single responsibility principle).
- Principio de abierto/cerrado (Open/close principle).
- Principio de sustitución de Liskov (Liskov substitution principle).
- Principio de segregación de la interfaz (Interface seggregation principle).
- Principio de la inversión de dependencia (Dependency inversion principle).
Fueron formulados y popularizados por Robert C. Martin en su artículo The Principles of OOD y se refieren al diseño de clases en programación orientada a objetos.
El objetivo de este capítulo es el de presentarlos para que pienses en ellos al escribir tu código Python. Escribir software respetando estos principios produce código menos rígido, permisivo al cambio, robusto y reutilizable.
Durante el bloque dedicado al desarrollo de software, revisitaremos de manera más formal los principios SOLID, una vez hayas escrito el suficiente código Python como para entender intuitivamente las motivaciones que nos llevan a aplicarlos.
Considera el siguiente fragmento:
class Compiler:
_translations_map = {
'+': 'AddOne',
'-': 'SubOne',
'>': 'Next',
'<': 'Previous',
'[': 'StartLoop',
']': 'EndLoop',
'.': 'Output'
}
def compile(self, source):
print('Compilation started')
total = len(source)
translation = []
for index, line in enumerate(source):
print(f'{index/total:.2%} completed')
if line in self._translations_map:
translation.append(self._translations_map[line])
print('Compilation finished')
return '\n'.join(translation)
if __name__ == '__main__':
program = '+++>++[-<+>].'
compiler = Compiler()
print(compiler.compile(program))
Una clase debería tener una, y sólo una, razón para cambiar.
La razón para cambiar se refiere a la respuesta a la pregunta "¿por qué ha cambiado esta clase?". Dar más de una respuesta distinta es síntoma de que la clase se está haciendo demasiado compleja.
-
Indica cómo se rompe, a tu juicio, el principio de responsabilidad única en esta situación.
-
Propón alguna alternativa para paliar el efecto del acoplamiento de responsabilidades.
Algunas responsabilidades que deberías separar son:
- Mostrar el progreso de la compilación.
- Construir el listado de instrucciones.
- Emitir eventos.
Deberías ser capaz de extender el funcionamiento de una clase, sin modificarla.
La cualidad "abierta" de una clase se refiere a que deben existir los mecanismos adecuados para modificar el comportamiento de una clase. La cualidad "cerrada" de una clase prohíbe la modificación del código fuente.
-
Considera esta propuesta de separación de responsabilidades.
-
Queremos mostrar el progreso como una barra, en lugar de como un número. ¿Cómo podríamos hacerlo sin modificar las clases que ya se encargan de mostrar el progreso?
-
¿Qué otros cambios puedes anticipar para mitigar este problema en el futuro?
El LSP indica que los objetos de una superclase deberían ser reemplazables por objetos de alguna subclase sin romper la aplicación.
Esta es una formulación alternativa de la que viene en el artículo de Robert C. Martin, sugeria por Thorben Janssen en el artículo SOLID Design Principles Explained: The Liskov Substitution Principle with Code Examples.
Lo que significa que lo que podamos asumir del comportamiento de un tipo, debe ser asumible de un subtipo cualquiera.
-
Imagina un parser más estricto que fallara al encontrar un símbolo desconocido. ¿Estarías rompiendo el principio de sustitución de Liskov?
-
Considera el siguiente ejemplo:
class Rectangle: def __init__(self): self._width = 1 self._height = 1 def setWidth(self, width): self._width = width def setHeight(self, height): self._height = height def getWidth(self): return self._width def getHeight(self): return self._height def area(self): return self._height * self._width class Square(Rectangle): def __init__(self): self._width = 1 self._height = 1 def setWidth(self, width): raise NotImplemented() def setHeight(self, height): raise NotImplemented() def area(self): return self._height * self._width
-
¿Cómo implementarías
setWidth
ysetHeight
para los cuadrados? -
Considera el siguiente código. ¿Tiene sentido para un rectángulo? ¿Y para uno de tus cuadrados?
def test_rectangle_area(rectangle): rectangle.setWidth(5) rectangle.setHeight(4) assert rectangle.area() == 20, 'the area must be 20' test_rectangle_area(Rectangle()) test_rectangle_area(Square())
Debes depender de las abstracciones, no de las implementaciones.
El principio de inversión de dependencias dice que un programa no debería construirse sobre implementaciones sino dependender de las interfaces.
La inversión de dependencias es complicada de alcanzar en lenguajes dinámicamente tipados donde no se puede hacer explícita la dependencia con la interfaz. Una formulación alternativa, que funciona mejor con lenguajes dinámicos es:
Tus clases no deben crear objetos, sólo usarlos.
-
Supón que queremos probar que el compilador y sólo el compilador se comporta correctamente. Considera el siguiente parser, que realmente no procesa el fuente sino que devuelve una lista fija de prueba. ¿Cómo se te ocurre modificar el código para que el compilador utilizara este parser
"de pega".class FakeParser: def parse(self, _): return list('+-<>[].')
Crea interfaces granulares específicas de cada cliente.
Este principio establece que cada método debería recibir una "vista" o interfaz del objeto que lidiara únicamente con los aspectos en los que el método está interesado.
-
Considera las siguientes anotaciones:
class Parser: ... def parse(self, source: str) -> list: ... class Compiler: def __init__(self, parser: Parser): ...
¿Cómo crees que podrías hacer tu código más genérico? Fíjate en la funcionalidad que usas exactamente y sugiere anotaciones más específicas.