En cada nuevo reto en diseño software, no sé si por costumbre o porque ya empiezo a tener una edad, me viene a la cabeza la palabra SOLID, que aprendí cuando comenzaba mis andanzas en el mundo de la programación y ha seguido conmigo durante ya casi de 20 años. Pero… ¿Qué es SOLID?
Cuando comenzamos a diseñar un nuevo software ponemos todo nuestro esfuerzo, ilusión y cariño en crear una aplicación que solucione los problemas del cliente u organización que lo solicitó. En el momento de la entrega, cumple su cometido con creces. Todo son felicitaciones.
Pero… ¿Cuántas veces ha pasado que después de un tiempo utilizando vuestras herramientas, se decide ampliar funcionalidad con desarrollos que no tienen nada que ver con el uso al que se le da originalmente y por lo tanto la aplicación no está preparada para ello? Si a esto se le une la premura y los tiempos ajustados, estás aplicaciones acaban creciendo con evolutivos que hacen infinidad de cosas sin relación aparente, son enrevesadas y a las que cualquier cambio mínimo impacta de forma imprevisible en toda la solución.
Entonces, un software que inicialmente estaba bien pensado y realizaba su cometido, comienza a ser un quebradero de cabeza con cada nuevo evolutivo.
Cualquier programador estará de acuerdo conmigo en que el desarrollo de software no solo consiste en tirar líneas de código que funcionen. Hay que hacer un buen diseño que nos permita evolucionar la aplicación de forma limpia y sostenible incluso aunque se produzcan imprevistos.
Para conseguir un buen diseño de software se utilizan patrones que solucionan problemas comunes que a lo largo de los años han aparecido y a los que ya se han enfrentado otros programadores.
Hoy voy a escribir sobre de SOLID, que no es un patrón sino un conjunto de principios que nos ayudarán a la hora de enfrentarnos al diseño de un software.
¿A qué nos referimos cuando hablamos de SOLID?
Podemos decir que son un conjunto de principios que sirven como base a la hora de elegir una arquitectura de software para realizar nuestras aplicaciones.
Estos principios, “Design Principles and Design Patterns”, fueron definidos por Robert C. Martin al principio del año 2000.
Si aún no lo conoces, te darás cuenta después de leer este post, de que SOLID es 20 años después lo que deberíamos intentar aplicar en nuestros proyectos para hacer código de calidad.
Los 5 principios que todos debemos conocer
Cada letra de SOLID representa un principio y siguiéndolos conseguiremos hacer un código limpio, escalable y fácil de mantener.
- S: Single responsibility principle o Principio de responsabilidad única.
- O: Open/closed principle o Principio de abierto/cerrado.
- L: Liskov substitution principle o Principio de sustitución de Liskov.
- I: Interface segregation principle o Principio de segregación de la interface.
- D: Dependency inversion principle o Principio de inversión de dependencia.
A continuación, revisaremos cada uno de los principios para comprenderlos y tratar de tenerlos en la cabeza cada vez que tengamos que enfrentarnos a un desarrollo.
SOLID : Single responsibility principle
El Principio de responsabilidad única nos indica que una clase debería tener una razón única para cambiar, o lo que es lo mismo, cada clase dentro de su código debería tener una única tarea que realizar.
En mi opinión hay que tener cuidado con esta afirmación, ya que llevada a extremos puede generar un código difícil de seguir. Más que de “única tarea” me gusta hablar de “única responsabilidad”, entendiendo responsabilidad como una o un conjunto de tareas relacionadas.
Imaginaros un Product Owner y un Programador. Cada uno tiene sus responsabilidades diferenciadas. Dentro de esas responsabilidades hacen más de una tarea pero siempre coherentes y relacionadas con su puesto.
Product Owner (Tareas orientadas al negocio)
- Estudiar las necesidades del negocio
- Definir tareas que den valor al producto.
- Priorizar implementaciones para ser más competitivos a nivel de negocio
- Preparar Backlog del proyecto.
- …
Programador (Tareas orientadas al desarrollo de aplicaciones)
- Llevar seguimiento de sus tareas.
- Programar aplicaciones
- Testear sus desarrollos.
- Refactorizar
- …
Si tareas tan dispares, se entremezclan y recaen en una única persona, cualquier imprevisto en las tareas de desarrollo, seguramente impactaría en el tiempo de dedicación a las tareas orientadas a negocio y viceversa.
Pues bien, esta lógica hay que extrapolarla al código.
Ejemplo:
Un usuario está haciendo la compra online en la sección de frutería.
Como podéis observar primero el usuario quiere consultar el precio de las naranjas y finalmente lo añade al carrito de la compra por medio de la clase Orange.
Ahora vamos a analizar la clase Orange. Esta clase tiene dos responsabilidades:
- Gestionar las propiedades de la clase naranjas.
- Guardar en Base de datos la cantidad de naranjas que los usuarios compran.
Este código no cumple el principio de responsabilidad única.
Imaginaros que hay que realizar modificaciones a nivel de base de datos… probablemente habría que implementar dentro de la clase Orange nuevos métodos, propiedades, etc. Cualquier error aquí, podría hacer que la clase entera dejara de funcionar, impactando en el resto de responsabilidades que tuviera implementadas.
Otra pregunta que nos podemos hacer es la siguiente:
¿Guardar artículos en el carrito de la compra puede ser común a otras clases de frutas o incluso otros alimentos?
Si la respuesta es afirmativa, al programarse de esta manera comenzaremos a usar el famoso “copy/paste” en las nuevas clases que implementemos y sean susceptibles de interactuar en Base de datos.
Solución aplicando el principio de responsabilidad única:
Pues como nos recomienda el principio, debemos separar las responsabilidades en clases diferentes.
Como veis, ahora tenemos dos clases diferenciadas:
La clase Orange que gestiona las propiedades de las naranjas.
La clase ShoppingCard que interactúa con la Base de datos.
¡Ahora el código cumple el principio de responsabilidad única!
Aplicando este principio nuestro estaremos generando un código más limpio y fácil de mantener.
SOLID : Open/closed principle
El principio de abierto cerrado dice que una clase, modulo o función debe estar abierto para la extensión y cerrada para modificación.
Este principio nos indica que si queremos añadir código nuevo lo ideal sería añadirlo sobre lo ya existe sin tener que hacer grandes modificaciones.
Para conseguir esto lo mejor es hacerlo a través del uso de la herencia.
Imaginaros un transportista que conduce de Camión y transporta alimentos de una cooperativa a los mercados de Madrid. Se expande el negocio a África. Ahora necesitan que sus productos vayan en avión a África. Para ello han comprado una avioneta.
Pues bien, podrían pedirle al conductor de camiones que aprenda a pilotar aviones. Pero implica ir a una academia, modificar los comportamientos, las rutinas y habilidades del conductor y tendría que adaptarse a los nuevos cambios en su trabajo en un proceso muy costoso.
¿Qué es lo más lógico?
Que contraten un piloto de avionetas para transportar los alimentos.
Extrapolemos esta lógica al código…
Ejemplo:
Un usuario ha llenado la lista de la compra en la frutería online y quiere ver el total de los artículos seleccionados.
A través de la lista de la compra queremos ver el total de la misma.
Este código no cumple el principio de Abierto/Cerrado.
Como podéis observar, por cada nueva fruta que hubiera habría que modificar el método GetPriceInEuro de la clase ShoppingCard para añadir su precio por lo que no cumple el principio según su definición.
Incrementar el comportamiento de un módulo no debería tener como resultado cambiar el código original, es decir, el código original debería permanecer intocable.
¿Cómo recomienda el principio que debería hacerse?
Solución aplicando el principio de abierto cerrado:
Creamos una clase abstracta para obligar al resto de clases que hereden de ella tener que implementar el método GetPrice().
A continuación, simplemente generamos una clase por cada tipo de fruta que hereden de la clase Fruit.
De esta forma solo habría que implementar una nueva clase por cada tipo de fruta que hubiera que dar de alta en el sistema sin tener que modificar ninguna otra clase.
Siguiendo este principio conseguimos que la aplicación sea más limpia y escalable.
SOLID : Liskov substitution principle
El principio de sustitución de Liskov indica que deberíamos poder utilizar cualquier clase derivada en lugar de una clase base y hacer que se comporte de la misma manera sin modificaciones.
Para aplicar este principio simplemente hay que preguntarse: ¿Es siempre un?
Apliquemos el principio a la vida real. Imaginemos las figuras de: Empleado, director, jefe de equipo y programador.
Dicho lo anterior la jerarquía quedaría de la siguiente manera:
Extrapolemos este principio al código.
Ejemplo de uso del principio de sustitución de Liskov:
Crearemos una clase Employee de la que heredarán los diferentes individuos que componen la empresa.
Ahora creamos los tres tipos de empleados que heredan de la clase Employee.
Ahora veremos tres ejemplos en el que se confirma el principio de sustitución de Liskov en el código:
Ejemplo 1: El objeto Employee puede almacenar objetos de cualquier tipo de los creados (Director, Programmer, TeamLead).
Ejemplo 2: Una vez instanciados los objetos. La variable de tipo Employee puede almacenar variables de tipo Julia.
Ejemplo 3: Se trata de un caso en el que tenemos que meter en una lista a todos los empleados de la empresa sin importar el tipo.
Cumpliendo con este principio se confirmará que la jerarquía de clases de nuestra aplicación está bien construida y es fácil de entender.
SOLID : Interface segregation principle
El principio de segregación de la interface nos indica que no se debe implementar interfaces con métodos que no se usan.
Este principio nos está recomendando que no debemos definir interfaces con muchos métodos sino con pocos y muy relacionados.
Supongamos que estamos en un Zoológico y estamos leyendo las fichas informativas de los animales. Cada ficha tiene unas propiedades comunes y otras específicas de cada tipo de animal. Por ejemplo:
Propiedades comunes:
Comer. Porque todos los animales se alimentan.
Propiedades no comunes:
Nadar. Esto es propio de los animales acuáticos y no tendría sentido que estuviera en la ficha informativa de los animales terrestres.
Ejemplo:
Vamos a catalogar animales… Existe una primera clase llamada Mamiferos que heredarán todas las clases de animales que se correspondan a ese tipo.
Tenemos también las clases: Gorila, Perros y Ballena.
Como podemos observar heredan de la clase Mamiferos y tienen una intarface IMamiferos para obligar que las clases anteriores tengan que implementar varios métodos.
Este código no cumple el principio de segregación de la interface
El gorila, el perro y la ballena son mamíferos, pero si analizamos la Interface nos daremos que hay un método que es NumeroPatas que tiene sentido en un perro o el un gorila pero… ¿en una ballena? Las ballenas no tienen patas!.
Solución aplicando el principio de segregación de la interface
Este problema que se plantea, deberíamos solucionarlo simplemente creando otro interface para este tipo de animales de la siguiente forma.
Como veis, ahora si hay una interface IMamiferos común para todos los mamíferos y otra IMamiferosTerrestres también común para este tipo de animales.
Ahora sí se está cumpliendo el principio de segregación de la interface y estamos haciendo un código más limpio y coherente.
SOLID : Dependency inversion principle
El principio de inversión de dependencia tiene dos ideas de fondo:
- Los módulos de alto nivel no deberían depender de módulos de bajo nivel, sino de abstracciones.
- Las abstracciones no deberían depender de los detalles. Los detalles deberían depender de las abstracciones.
Con el paso del tiempo nuestra aplicación llegará a estar formada por muchos módulos o clases. En ese momento deberíamos usar inyección de dependencias para poder controlar las funcionalidades desde un sitio concreto en vez de tenerlas esparcidas por todo el código.
Ejemplo:
Imaginamos unos usuarios que se dan de alta en una tienda online. El responsable de la tienda nos indica que los usuarios que se dan de alta deben guardarse en un fichero de texto y en Base de datos.
Cuando el usuario se registra se invoca al método ExportData de la clase DataExporter que tiene la lógica para gestionar los datos.
Dicho lo anterior, como veis existen dos clases diferenciadas: FileData que se encarga de guardar la información en ficheros y DbData que se encarga de guardar los datos en base de datos.
Las clases FileData y DbData se utilizan mediante los métodos de la clase SaveData.
A lo largo el tiempo, la funcionalidad en SaveData crece exponencialmente según se va ampliando la funcionalidad de la clase DbData.
Años después nos dice el cliente que ahora los nuevos usuarios que se registren hay que notificarlos a una API para que se encargue otra aplicación de guardar los datos.
Tendríamos que cambiar todas las instancias de la clase SaveData!.
El código anteriormente expuesto genera un fuerte acoplamiento debido a que una clase de alto nivel SaveData depende de una clase de más bajo nivel DbData por lo que este código no cumple el principio de inversión de dependencia por lo que implica tener que rehacer el código y evita la reutilizar el mismo.
Para cumplir con el principio habría que hacerlo de la siguiente manera.
Solución aplicando el principio de inversión de dependencia
Creamos la interface IData y todas las clases que se encarguen de interactuar con distintos orígenes de datos.
- DbData: se encarga de interactuar con BD
- FileData: Interactúa con ficheros
- ApiData: Interactúa con Api.
En la clase SaveData, tal y como está compuesta ahora, se ha implementado el método que gestiona la interacción con la API (ApiData) sin tener que modificar ningún método.
Ahora en la clase DataExporter que contiene la lógica de interacción con los diferentes orígenes de datos solo tenemos que decirle de la siguiente forma a donde queremos dirigirnos para guardar los datos de forma limpia, clara y desacoplada.
Ahora sí cumplimos el principio de inversión de dependencia.
Si todavía os queda alguna duda sobre cómo se aplican estos principios, podéis descargaros el código que he realizado para explicar los ejemplos. Podréis tanto verlos en funcionamiento como modificarlos si lo deseáis.
Puedes descargarte aquí el Artefacto. Para descomprimirlo tienes que usar la siguiente contraseña: kabel.
Conclusión
Los principios SOLID son buenas prácticas que te pueden ayudar a escribir un mejor código: más limpio, flexible, coherente, sostenible y escalable.
No son verdades absolutas, pero el conocerlos y aplicarlos en el diseño de software siempre que podamos, nos ayudarán a anticiparnos a posibles problemas en el código a largo plazo.
En definitiva, SOLID no es la solución a todos los problemas pero debería ser ABC de cualquier programador o diseñador de software.
Nos vemos en el siguiente post.