¿Qué es SOLID?

SOLID es un acrónimo para 5 principios de POO (Programación Orientada a Objetos) que favorecen la creación de sistemas legibles, extensibles y mantenibles.
 

¿Por qué debería interesarme?

Los principios SOLID forman parte de las buenas prácticas de programación y seguirlos son una garantía de que tu código no va a crecer de forma descontrolada y caótica.

¿Te suena alguna de estas situaciones?

  • Un requerimiento cambia (aparentemente pequeño) y te encuentras con que tienes que cambiar un montón de ficheros.
  • Te piden cambiar algo y tienes que modificar algo que ya funcionaba, solo para darte cuenta más tarde de que ha dejado de ir.
  • Algo no funciona bien y no tienes ni idea de dónde puede estar el fallo.
  • Te ves cambiando constantemente las mismas clases/funciones una y otra vez.
  • A medida que pasa el tiempo y se producen cambios la legibilidad del código cae en picado.

¡Entonces te interesa aprender SOLID! 😉

 

Los principios

El acrónimo SOLID se forma a partir de la primera letra de cada principio en su forma inglesa:

Single responsibility principle
Open/closed principle
Liskov substitution principle
Interface segregation principle
Dependency inversion principle

 

1. Principio de responsabilidad única (Single Responsibility)

Una clase sólo debería tener una única razón para ser modificada.

Para cumplir con este principio, debemos dividir el código de forma que cada clase se encargue de una única funcionalidad bien definida (una única responsabilidad).

El objetivo de este principio es el de limitar el impacto de los cambios realizados sobre nuestras funcionalidades.

Si cada clase se encarga de una única funcionalidad, cuando haya que modificar dicha funcionalidad, solo habrá que modificar una única clase (generalmente pequeña) y, por lo tanto, será más fácil realizar el cambio y habrá menos riesgo de que dicho cambio tenga repercusiones sobre el resto del código.

Ejemplo:

Tenemos 5 clases y todas ellas hacen uso de una librería para generar informes. Si en algún momento del desarrollo decidiéramos cambiar esta librería por otra (más eficiente, más moderna o con nuevas funcionalidades) deberíamos modificar las 5 clases para adaptarlas al cambio. No obstante, si hubiéramos seguido este principio, hubiéramos detectado que el crear informes es una funcionalidad única y autónoma, por lo que hubiéramos creado una clase que se encargara de generar los informes; esta clase sería entonces la única que hiciera uso de la librería por lo que, al cambiar de librería, solo deberíamos modificar una única clase.

 

2. Principio de abierto/cerrado (Open/closed)

Las entidades de software deberían estar abiertas a la extensión pero cerradas a la modificación.

Para cumplir con este principio, debemos asegurarnos de que las clases/funciones/módulos que ofrecemos permiten extender su funcionalidad sin modificar su código.

Esto significa, por un lado, que estas clases deben estar “abiertas a la extensión”, es decir, que podemos hacer que la clase se comporte de formas nuevas y diferentes a medida que los requerimientos de la aplicación cambien. Y, por otro lado, que deben estar “cerradas a la modificación”, es decir, el código fuente de dichas clases debe ser inviolable, nadie debería verse forzado a modificar código existente para añadir nuevas funcionalidades.

La razón por la que existe este principio es debido a que modificar clases constantemente puede introducir errores y crear incertidumbre (nunca puedes dar ninguna clase por terminada), por lo que resulta conveniente que, para añadir nuevas funcionalidades, no sea necesario modificar código ya existente, probado y que funciona. Otra razón más para seguir este principio es que, en el caso de ofrecer nuestro código en forma de librería para terceros (un entorno en el cual el código fuente es realmente inviolable), si no damos herramientas a los clientes para que puedan adaptar el comportamiento de nuestra librería a sus necesidades, probablemente les resulte poco útil y no quieran usarla.

La forma de lograr esto es hacer que las funcionalidades de nuestra clase dependan de interfaces y que, al necesitar nuevas funcionalidades, podamos añadir el código de esas nuevas funcionalidades dentro de nuevas implementaciones de dichas interfaces. De este modo, en vez de modificar clases o implementaciones existentes, nos bastaría con crear nuevas clases (implementaciones) cada vez que queramos añadir algo nuevo.

Ejemplo:

Tenemos una clase encargada de generar informes en formato PDF. En en momento dado, los requerimientos cambian y necesitamos generarlos en formato Excel, por lo que modificamos la clase que ya teníamos. Al día siguiente de subir a producción descubrimos que ¡oh! la generación de informes ya no va, y es debido a algún cambio que hicimos. No obstante, si hubiéramos seguido este principio, no hubiéramos tenido que modificar código ya existente, si no que nuestra clase llamaría a métodos de una interfaz para generar informes y tendríamos una implementación de dicha interfaz para cada formato. Como una interfaz puede referenciar a cualquiera de sus implementaciones, el cliente elegiría el formato a usar en el momento de llamar a la clase (especificando la implementación a usar), sin necesidad de modificar el código fuente de dicha clase.

 

3. Principio de sustitución de Liskov (Liskov substitution)

Todos los objetos deberían poder reemplazarse por instancias de sus subtipos sin alterar el correcto funcionamiento del programa.

Para cumplir con este principio, debemos tener en cuenta que cada clase que hereda de otra debería poder usarse como su clase padre sin necesidad de conocer las diferencias entre ambas.

La idea detrás de este principio es que las clases hijo nunca deberían cambiar el funcionamiento natural del padre, ni diferir sustancialmente entre sí.

A menudo, casos de la vida real no se pueden traducir directamente a código, lo que lleva a violar este principio.

Ejemplo:

Tenemos una clase llamada Cuadrado (hijo) que hereda de otra llamada Rectángulo (padre). La clase Rectángulo tiene métodos públicos para modificar su altura y anchura (por separado), pero como Cuadrado hereda de ella, significa que Cuadrado también tendrá métodos para modificar su altura y anchura por separado, cosa que no puede ocurrir. Si hubiéramos seguido este principio, hubiéramos hecho la prueba de intentar reemplazar mentalmente todas las instancias de Rectángulo por sus hijos (Cuadrado) y evaluado si el cambio tenía sentido; en seguida hubiéramos visto que un cuadrado no debería poder tener altura y anchura diferentes, por lo que hubiéramos buscado una alternativa.

 

4. Principio de segregación de la interfaz (Interface segregation)

Muchas interfaces específicas son mejores que una interfaz de propósito general.

Para cumplir con este principio, debemos tener en cuenta que las clases no deberían estar forzadas a implementar métodos que no utilizan.

O lo que es lo mismo: no debemos pensar en interfaces que lo engloban todo, sino en interfaces que resuelven problemas muy concretos.

Ejemplo:

Tenemos 2 clases que comparten 5 métodos, por lo que hacemos que ambas clases implementen la misma interfaz (la cual define esos 5 métodos). Más tarde queremos añadir una nueva clase, muy similar a las 2 anteriores, pero la cual solo utiliza 3 de los 5 métodos, por lo que ahora tenemos un problema, o bien hacemos que la nueva clase implemente la misma interfaz y nos veamos obligados a implementar métodos que sería erroneo que utilizara, o bien subdividimos arbitrariamente la interfaz para acomodar a la nueva clase. Ninguna solución es satisfactoria. No obstante, si hubiéramos seguido este principio, no hubiéramos creado interfaces englobadoras de forma sistemática, sino que hubiéramos identificado los funcionalidades atómicas de nuestra aplicación y hubiéramos creado interfaces específicas para implementar el conjunto de métodos necesarios para resolver cada una de dichas funcionalidades. De esta manera, no importa cuantas de esas funcionalidades haga uso una clase, siempre existirá una composición de interfaces que permita implementar cualquier combinación de ellas.

 

5. Principio de inversión de la dependencia (Dependency inversion)

Se debe depender de abstracciones, no de implementaciones.

Para cumplir con este principio, debemos asegurarnos de no usar nunca referencias a clases concretas a menos que sea estrictamente necesario, sino intentar usar siempre abstracciones (interfaces o clases abstractas) y siempre la abstracción más general posible.

La intención de este principio es la de evitar tener que cambiar código simplemente porque un objeto del cual dependíamos deba cambiarse por uno diferente (aunque realice la misma función), reduciendo efectivamente el acoplamiento (o dependencia) de unas clases con otras.

Ejemplo:

Tenemos una clase con una gran cantidad de métodos que reciben por parámetro una implementación concreta de listas (p.e. ArrayList en Java). En un momento dado necesitamos hacer estas mismas operaciones pero con objetos de otra implementación de listas (p.e. LinkedList en Java), como todos nuestros métodos recibían solo una de las implementaciones por parámetro, nos vemos obligados a duplicar los métodos que ya teníamos y cambiarles el tipo de sus parámetros para adaptar la clase a sus nuevas necesidades. No obstante, si hubiéramos seguido este principio, siempre hubiéramos usado las referencias a los tipos más abstractos posibles, por lo que los parámetros de nuestros métodos no serían de tipo concreto (p.e. ArrayList  o LinkedList) si no uno más general (p.e. List, ArrayList y LinkedList ambos heredan de List).

 

Conclusión

Como hemos visto, los principios SOLID son directrices fáciles de recordar y aplicar pero que mejoran sustancialmente la calidad de cualquier software y ayudan a evitar problemas comunes. Seguir estos principios confiere, además, la garantía de que nuestro código será siempre altamente legible, ampliable y mantenible, evitando así el caos que suele aparecer en proyectos grandes.

Fotografia de capçalera de Scott Dougall on Unsplash
código,desarrollo,Open,Programación,SOLID,