Principios SOLID y patrones de diseño

Cuando empecé a estudiar diseño y arquitectura de software, uno de los primeros conceptos con que me encontré fue el de los principios de diseño SOLID.

Me parecieron unas ideas muy interesantes, pero no tan fáciles de aplicar.

Luego aparecieron los patrones de diseño, y todo empezó a tener sentido. Los patrones me dieron estrategias puntuales para resolver problemas específicos en mi código. Mi forma de programar cambió radicalmente luego de conocer los patrones de diseño.

Con el tiempo entendí que los patrones de diseño son un elemento importante para hacer aplicables los principios SOLID.

En este artículo pretendo darte una revisión básica de cada principio y los patrones de diseño que ayudan a cumplirlo.

Introducción a los principios SOLID

Estos principios fueron definidos por Robert Martin (mejor conocido como el tío Bob). Su trabajo consistió en tomar las ideas de distintas personas de la industria, y unirlas bajo la sigla SOLID (cada letra representa un principio).

¿Existen más principios de diseño? Claro que sí.

Sin embargo, estos son los más conocidos y aplicados en el diseño orientado a objetos.

Vamos por el primero.

Principio de única responsabilidad

Single Responsibility Principle.

Este principio dice que nuestro módulo (clase) debería tener una sola razón para ser modificado. Veámoslo con un ejemplo.

La clase Mensajero, en la siguiente imagen, tiene dos responsabilidades: enviar correos y enviar mensajes de texto.

Esta clase podría cambiar por varias razones:

  • El mecanismo para enviar correos es diferente. Por ejemplo, antes se usaba un servidor SMTP y ahora se requiere usar un API.
  • Cambios en la plataforma de mensajes de texto.
  • Aparición de una nueva forma de enviar mensajes.

Acabamos de ver 3 razones diferentes por las cuales la clase podría cambiar. Esto nos puede traer problemas de mantenibilidad. De ahí la importancia de respetar este principio.

En el ejemplo anterior, podríamos utilizar distintos patrones de diseño para resolver el problema. En este caso en particular, vamos a separar cada una de las formas de mandar mensajes en clases diferentes, y luego, mediante un patrón de fachada (o façade) podemos facilitar el consumo de cada una de las funcionalidades.

Luego de los cambios, el diagrama de clases se vería así:

Así, cada clase tendría una única razón para cambiar. Por ejemplo, si en algún momento cambia la plataforma de mensajes de texto, solo tenemos que modificar la clase EnvioSMS. Y si en algún momento pasamos de enviar correos con un servidor SMTP a un API de un tercero, solo impactamos la clase EnvioCorreo.

De esta forma, estamos cumpliendo el principio de única responsabilidad.

Principio de abierto cerrado

Open/Closed Principle

Este segundo principio nos dice que nuestro sistema debería estar cerrado a modificaciones, y abierto a extensiones.

En pocas palabras: si queremos añadir nuevo código, lo ideal sería poder construir sobre lo que ya existe, sin tener que hacer modificaciones grandes.

Trabajemos sobre el ejemplo del principio anterior.

Supongamos que ahora queremos enviar mensajes usando el chat corporativo de la empresa. Tal como quedó el refactor del ejemplo anterior, serían necesarios los siguientes cambios:

  1. Crear una nueva clase llamada EnvioChat.
  2. Agregar un nuevo método en la clase Mensajero para enviar mensajes por chat.
  3. Crear una instancia de EnvioChat en la clase Mensajero.

Los cambios se verían así:

Esto no sería lo más óptimo. Cada vez que añadamos una nueva forma de envío, sería necesario modificar la clase Mensajero. Además, habría que cambiar todos los lugares donde se llamen los métodos de Mensajero para que tengan en cuenta la nueva forma de enviar mensajes.

Este es un ejemplo donde no estamos respetando el principio de abierto cerrado.

¿Cómo lo podemos mejorar?

Añadiendo mayor flexibilidad.

Flexibilidad que nos permita añadir nuevos mecanismos de forma fácil, sin tener que modificar lo que ya funciona.

Allí, de nuevo, vienen los patrones al rescate.

Lo primero que podríamos hacer es crear una interfaz que nos permita definir el comportamiento de cualquier clase que sea capaz de enviar un mensaje. Luego, ponemos a cada una de las clases especializadas en envíos a que la implementen.

(Notarás una nueva clase, Mensaje, que sirve para unificar los parámetros que recibe el método enviar).

Si estás familiarizado con el patrón adaptador, algo muy similar es lo que verás en ese diagrama. Cada clase está especializada en conectarse con una forma diferente de enviar mensajes.

Lo que necesitamos ahora es quitarle el conocimiento a la clase Mensajero de las distintas clases que envían mensajes. Para esto, podemos implementar una fábrica.

Teniendo la fábrica, la clase Mensajero ya no necesita métodos específicos por cada método de envío. Solo necesita pedirle a la fábrica un objeto de tipo EnvioMensaje, e invocar sobre este el método enviar.

Principio de sustitución de Liskov

Liskov subtitution principle

Este principio nos habla específicamente de la herencia. En pocas palabras, dice que siempre deberíamos poder reemplazar instancias de una clase padre por instancias de una clase hija, sin que hayan comportamiento no deseados.

Visto de otra manera, las clases hijas no deberían modificar o eliminar los comportamientos definidos en el padre.

A diferencia de los principios anteriores, no hay algún patrón de diseño específico por aplicar aquí. Sin embargo, tener a la mano patrones de diseño relacionados con la herencia pueden ser muy útiles. Algunos ejemplos:

  • Método plantilla.
  • Composite.
  • Estrategia.
  • Estado.

Principio de segregación de interfaces

Interface Segregation Principle

Debemos tener cuidado con definir interfaces con muchos métodos. De acuerdo a este principio, es mejor tener interfaces pequeñas, con pocos métodos muy relacionados (alta cohesión), en lugar de tener interfaces voluminosas que obligan a definir muchos métodos a quien las implementa.

Aquí el principio de única responsabilidad resulta muy útil, para separar esas interfaz de la mejor manera.

Algunos patrones, especialmente los de comportamiento, pueden ser muy convenientes:

  • Estrategia.
  • Estado.
  • Cadena de responsabilidad.

Principio de inversión de dependencias

Dependency Inversion Principle

Este principio tiene dos ideas importantes de fondo.

  1. Los módulos de alto nivel no deberían depender de módulos de bajo nivel, sino de abstracciones.
  2. Las abstracciones no deberían depender de los detalles. Los detalles deberían depender de las abstracciones.

Una forma de lograr esto es empleando la inyección de dependencias (un tipo de inversión de control). Si bien no es uno de los patrones de diseño orientado a objetos, es una forma muy útil de garantizar la dependencia con abstracciones.

En general todos los patrones que ayuden a abstraer nuestros módulos son útiles para lograr este principio. En particular, es relevante mencionar:

Conclusiones

Ni los principios SOLID ni los patrones de diseño son estrategias a prueba de bala para nuestras aplicaciones. Siempre existirán retos que requerirán de toda nuestra capacidad analítica como programadores y arquitectos.

Sin embargo, pensar siempre en los principios SOLID cuando estemos aplicando patrones de diseño, es una gran ayuda para guiar mejor nuestro trabajo.

Artículo escrito por Manuel Zapata. Manuel se desempeña como desarrollador y arquitecto de software. Hace unos meses lanzó su sitio web donde está ofreciendo un mini curso gratuito de principios de diseño (haz click aquí para inscribirte).

 

One thought to “Principios SOLID y patrones de diseño”

  1. El tio bob es muy bueno explicando esto. pero no son reglas estrictas, sino sugerencias. por ejemplo en el primer caso de principio de unica responsabilidad. el objeto mensaje que envia un sms y un email, si al final del dia necesitas modificar una de sus funciones igual vas a modificar la misma cantidad de codigo si esta en una sola clase o en 2 clases.
    y en el caso de del principio abierto cerrado. si necesitas añadir una nueva funcion de enviochat, da igual si lo haces sobre la misma clase. si al final todos los metodos se ejecutan en varias partes del sistema. la clase mensaje puede tener su propio metodo envio, que ejecuta los 3 metodos internos… ahorrandote un factory. y teniendo en una clase todos los metodos relacionados a mensajes. que es mas practico que tener 3 clases distintas y un factory.

    Lo importante es que la clase Mensaje cumpla la/s funciones de enviar mensajes. y no que haga algo como enviar mensaje y loguear al usuario.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *