S.O.L.I.D.

Un artículo de Rafa G. Blanes

¿Cómo conseguir que un proyecto software sea mantenible y que se pueda modificar con sencillez?

Esta pregunta tiene muchas respuestas, según la naturaleza del mismo proyecto, claro. No obstante, existen una serie de principios que, de seguirlos, la vida (y estructura) de un proyecto puede cambiar de una forma radical.

Demasiado a menudo veo proyectos, sin ir más lejos, muchos repositorios en GitHub, en los que es imposible discernir cómo se han implementado los principios de diseño más básicos en ingeniería del software, y no es que esté mal, claro, todo depende de la vida que vaya a tener ese proyecto.

No hace falta que conozcamos al detalle todos y cada uno de los patrones de diseño que más o menos son conocidos y aceptados por la industria y muy bien explicados en libros clásicos como este, pero, desde luego, si lo que queremos es que nuestro proyecto pueda ser mejorado continuamente sin demasiada dificultad, entonces sí que hay que seguir unas mínimas normas de desarrollo.

Aquí es donde entran los principios S.O.L.I.D.

Si tu proyecto tienen clases o funciones extraordinariamente largas del orden de cientos de líneas de código, si hay métodos con innumerables if o con muchos bloques anidados, o hay métodos o funciones con muchos parámetros, entonces tu aplicación camina hacia una solución espagueti difícil de mantener y evolucionar. 

Si es así, sigue leyendo...

¿Qué diferencia un principio de diseño de un patrón?

Mientras que un patrón es una solución técnica y clara a un problema particular, un principio es una guía que debe cumplir tu código a nivel de diseño. Los mejores frameworks de desarrollo son los que mejor cumplen con estos principios, y estoy pensando sin ir más lejos en Seneca, un framework que me gusta mucho para construir microservicios en NodeJS, aunque hay muchos otros ejemplos.

A grandes rasgos, los principios S.O.L.I.D. permiten diseñar una aplicación de un modo que:

  • Tenga un alto grado de desacoplamiento entre sus partes funcionales.
  • Se pueda extender en funcionalidad con sencillez, sin grandes cambios en la aplicación.
  • Es testeable.
  • Mejora la reusabilidad del código.

Aunque su implementación es más natural en programación orientada a objetos, se pueden aplicar en lenguajes funcionales. En el contexto de este artículo, hablaremos de entidades software para referirnos a clases, módulos, métodos, funciones, etc.

A continuación indico un breve resumen de estos cinco principios, junto con una técnica igualmente relevante, y que es la inversión de control.

Single-responsability principle (SRP o principio de responsabilidad única)

Una clase se debe dedicar a resolver un único problema, esto es, no se debe implementar una clase cuya funcionalidad mezcle ámbitos de la aplicación diferentes.

Si se utiliza bien este principio, entonces se podrán generar clases más o menos pequeñas y, por tanto, más reutilizables y fáciles de modificar y probar.

En el contexto de este principio, se entiende por responsabilidad como la razón para cambiar, esto es, si en una clase existe más de un motivo por el que puede cambiar, entonces es que no sigue este principo.

Quizá no sea simple de ver al comienzo, pero en este snippet de código indico un ejemplo sencillo (y algo académico), escrito en javascript para NodeJS.

Open-closed principel (OCP o principio abierto/cerrado)

Según este principio, una clase se debe diseñar de modo que sea fácilmente extendible sin modificar su funcionalidad actual. Fácil de describir, pero para implementarlo hace falta cierto grado de abstracción. Según la característica de la aplicación en la que se esté trabajando este principio se puede aplicar o no.

No niego que este principio sea algo sutil, pero si se entiende bien, en realidad lo que está indicando es que la entidad software debe abstraer al máximo el core de la funcionalidad que implementa.

Lo que persigue conseguir OCP es extender la funcionalidad de una entidad software sin modificar su código, y para ello, como digo, hace falta cierta abstracción.

En este otro snippet de código indico cómo aplicar este principio en el clásico ejemplo de validación de entradas de usuario.

Liskov substitution principle (LSP o principio de sustitución de Liskov)

Este es quizá el principio más sutil de entender, pero igualmente importante. Con LSP lo que se pretende es asegurarnos de que se implementa correctamente la herencia entre clases.

Entendemos como clase hija o extendida aquella que deriva de otra.

La definición es un tanto académica, de modo que haré una descripción más sencilla; lo que viene a decir LSP es que si un programa utiliza una clase, entonces deberá seguir siendo válido (= funcionará bien) si se la sustituye por una nueva clase que derive de ella. Si un programa utiliza la clase P, y en su lugar sustituimos P por O (donde O deriva de P), entonces, el programa deberá seguir funcionando correctamente.

Y aquí la definición de la autora (Bárbara Kiskov): si o1 es un objeto de tipo S, y o2 es un objeto de tipo T, si el comportamiento de un programa (función, método, etc.) basado en objetos T, usando o1 y o2 no cambia, entonces necesariamente S es un subtipo de T.

Puff..., en realidad está indicando cómo hay que implementar la herencia entre clases.

Sencillo de decir, no tan obvio de saber implementar, aunque con este principio nos garantizamos que la herencia se hace bien y además las clases base (padre) y sus derivadas (hijas) son más fácilmente testeables.

LSP está muy relacionado con el principio OCP, ya que establece también que una clase hija debe extender el comportamiento de la padre, no modificarlo.

Para implementar LSP hace falta un grado alto de abstracción de la solución que pretendemos resolver; si se rompe este principio en nuestro diseño, entonces es que no se está abstrayendo lo suficientemente bien.

Aquí dejo un ejemplo ilustrativo que cumple LSP.

Por último, una forma de forzar que se cumple este principio es usar diseño por contratos, en el que se especifican interfaces y las clases base las implementan.

Interface segregation principle (ISP o principio de segregación de interfaz)

Este principio pretende evitar la creación de interfaces extensas con métodos que no tienen nada que ver unos con otros, de modo que una clase que derive de ellas tenga que dejar sin implementar algunos de esos métodos porque no los necesita. 

Con ISP lo que se sugiere es dividir esta interfaz (fat interface) en interfaces más pequeñas y coherentes, de modo que las clases que deriven de ellas lo hagan porque necesitan todos y cada uno de sus métodos.

La consecuencia de este principio es que necesariamente las interfaces que se definen en el proyecto tienen un alto grado de abstracción y de granularidad.

En este otro ejemplo, se puede ver cómo aplicar este principio.

Dependency inversion principle (DIP o principio de inversion de dependencias)

Este principo es, quizá, el más popular de todos y el más fácil de implementar y en ocasiones se le confunde con la inyección de dependencias.

Lo que viene a decir es que nuestro código (en un módulo, una clase, etc.) debe depender de abstracciones, no de instancias concretas de las mismas.

Entendemos por abstracción la definición que debe implementar una clase concreta, lo que viene a ser lo mismo que clases abstractas e interfaces en OOP.

Con DIP conseguimos que el código esté más desacoplado y que las unidades de funcionalidad sean más pequeñas (y, por tanto, reutilizables y testeables).

Inversion of control (IoC o inversión de control)

La inversión de control no está dentro del conjunto S.O.L.I.D pero sin embargo está en la línea de conseguir código más desacoplado y mantenible, sin embargo; no es aplicable a cualquier tipo de proyectos.

Tradicionalmente, podemos decir muy simplificadamente que la implementación de un programa consiste en la ejecución de una llamada a un método o función tras otro. Sin embargo, a medida que este programa crece, es necesario estructurar la aplicación en módulos, entran en juego diversas arquitecturas, etc.

Es decir, el flujo habitual de una aplicación consiste en llamar a procedimientos (funciones o métodos) de una biblioteca.

Se implementa la inversión de control cuando se invierte ese flujo, esto es, cuando es la misma biblioteca la que llama al código del usuario que es el que implementa realmente la aplicación.

Parece enrevesado, pero hay multitud de ejemplos que implementan inversión de control y, además, es el enfoque natural cuando necesitamos que nuestra aplicación sea fácilmente extensible. Sin embargo, insisto en decir que no siempre es necesario implementar este mecanismo, depende de la naturaleza del proyecto.

IoC y DIP (inversión de control e inversión de dependencias) suelen ir de la mano, ya que es sencillo implementar IoC utilizando DIY:

Mis libros en todas las tiendas:

Amazon
Google Play
Apple
Kobo
Barnes and Noble
Scribd
Smashwords
Payhip
Gumroad

Rafael Gómez Blanes Rafael Gómez Blanes
¿Hablamos?

 

Trabajo en...

Archivo

Mis novelas...