Principios SOLID con ejemplos. Parte I

Qué son los principios SOLID y porqué te ayudarán a desarrollar software de forma correcta y mantenible
Principios SOLID con ejemplos. Parte I

Todos empezamos programando como buenamente podemos, lo que generalmente produce código espagueti; o lo que es lo mismo, código que funciona de forma automágicamente y que nadie se atreve a meter mano. Con el paso del tiempo, nos tocará hacer modificaciones en el código y, a parte de no saber por donde empezar, lo que arreglemos por un lado se romperá por el otro. Esta es la casúistica de este tipo de códigos.

Hoy voy a intentar darte pistas y formas de evitar que esto suceda que, junto con las pruebas de software, harán tu código mucho más fácil de mantener, entenderás porqué funciona y podrás asegurarte de que nada se rompa. Las pruebas las dejamos para otra entrada del blog, así que vamos a ver qué es eso de SOLID y porqué es "mejor" para tu código.

Entendiendo SOLID

SOLID no es más que el acrónimo que forman los nombres de los principios cuando los juntas en una palabra (en inglés):

  • S de Single Responsability, o Responsabilidad única
  • O de Open/Close, o Abierto/Cerrado
  • L de Liskov Substitution, o Sustitución de Liskov
  • I de Interface Segregation, o Segregación de interfaces
  • D de Dependecy Inversion, o Inversión de dependencias

Realmente todos son importantes, pero en mi opinión, cuando estás empezando hay que tener muy claros los dos primeros para empezar a diseñar buen software. Los otros tres son más importantes cuando ya has asumido los dos primeros. Te los voy a contar todos.

Te estarás preguntando, ¿cómo escribir más líneas, hacer más clases, y en definitiva, trabajar más de lo estrictamente necesario puede mejorar mi código? Te lo cuento a continuación.

Ejemplo abstracto, pero que a todos nos ha pasado

Imagina que tienes un trozo de código que hace la siguiente misión: cuando se le llama, hace una consulta a una API sencilla, que devuelve un número. Si ese número es un 1, enviamos un email a una dirección de correo electrónico (que previamente hemos configurado). Sencillo no? Eso lo puedes hacer en una sola función, más o menos así:

//Pseudocódigo

function sendMail() {
  var respuesta = fetch('http://www.miapi.es/check_mail');
  if (respuesta == 1) {
    mailer = new Mailer('midirecció[email protected]', 'Asunto', 'Mensaje');
  }
}

Y tú dirás, "No veo cómo hacer esto más simple". ¡Ya! Deja que sigamos.

Con el paso del tiempo, en un día, un mes o un año, tendrás que hacer un ajuste (ten paciencia, siempre te tocará cambiar algo). Este ajuste es que si el número que devuelve la API es un 2, tendremos que enviar un segundo email a otra dirección. Vaya, bueno, pues crearé una función que le pases los parámetros y envíe el mail y de paso, si mañana otras partes del código tienen que enviar un email, que usen esta función.

//Pseudocódigo
function realmenteEnviarElMail(direccion, asunto, mensaje) {
  return new Mailer(direccion, asunto, mensaje);
}

function sendMail() {
  var respuesta = fetch('http://www.miapi.es/check_mail');
  if (respuesta == 1) {
    realmenteEnviarElMail('midirecció[email protected]', 'Asunto', 'Mensaje');
  }
  else if (respuesta == 2) {
    realmenteEnviarElMail('otradirecció[email protected]', 'Asunto', 'Mensaje');
  }
}

Bueno, no está mal. Pero espera. El jefe, que es quien paga los emails, decide que mejor enviamos notificaciones de Telegram o Slack (que es gratis). Además, la API (que es de un tercero) ha cambiado y ya no es igual (porque sí, las APIs cambian, te lo aseguro). Entonces tenemos que hacer un cambio en nuestra función que enviaba emails borrando literalmente la parte que enviaba emails. Lo podemos cambiar por algo que envíe notificaciones de Telegram o Slack, pero... ¡¡¡ También tienes otras partes de tu código que envían emails con esa función !!!

Conclusión: tienes una función que originalmente enviaba emails que ahora no manda emails. También tienes una función auxiliar en una clase que antes se utilizaba para mandar emails desde la función principal pero que ahora la utiliza todo el mundo menos la propia función principal. Lo más gracioso de todo es que seguramente la clase donde estaban estas funciones se llama EnviarMail. 😂

Seguro que ya has visto el problema principal. Lo que era una pequeña función que hacía una tontería ha ido creciendo de forma desproporcionada. Ahora tiene muchos "if", muchos caminos diferentes (que tienes que probar), y es un verdadero lío: ¡no se entiende a simple vista!

Y es entonces cuando te acuerdas de SOLID y te das cuenta en tus propias carnes de porqué deberías haberlo empleado antes de desarrollar tu función principal en 3 minutos.

Vamos a ver qué es cada una de las iniciales de SOLID y porqué te van a salvar la vida (o al menos, hacértela más fácil).

Principio de responsabilidad única

Memoriza esto, pues para mi es más importante que un mandamiento de la programación: "Una clase solo se encarga de una tarea". Te la resumo un poco más para que te quede más claro: "Una clase = una cosa". Es básicamente lo que dice este principio: una clase solo tiene una responsabilidad que debe hacer de forma correcta.

Recuerdas nuestro ejemplo, realmente había 3 tareas: obtener de una API, cotejar los resultados con un criterio propio y enviar un mail. Pues ya sabes, si hay 3 tareas tienes que hacer 3 clases. Utilizando la S de SOLID, el código queda más legible. A continuación puedes ver el pseudocódigo de la nueva función y de las tres clases auxiliares que emplea. Fíjate que la clase General, que es la que coordina el proceso, detalla perfectamente qué hace: obtiene de una api, comprueba criterios y envía un mail.

class ObtenerDeAPI {
    //... Haz lo que tengas que hacer para obtener los resultados.
}
class ComprobarCriterios {
  //... Determina si se manda el mail.
}
class EnviarMails {
  //... Haz lo que tengas que hacer para enviar emails.
}

class General {
  function main() {
    var response = new ObtenerDeAPI();
    var criterio = new ComprobarCriterios();
    if (criterio) {
      new EnviarMails()
    }
  }
}

De esta forma, si mañana el jefe te dice que ya no quiere enviar emails, solo tendrás que cambiar new EnviarMails() por new Telegram(). Y el resto de código podrá seguir usando la clase que nació para enviar emails exclusivamente.

Principio de Abierto/Cerrado

Este principio básicamente dice que una clase nunca debe ser modificada; en su lugar deberás extenderla. Realmente abierta a modificaciones quiere decir que en la medida de lo posible uses ámbitos de visibilidad public o protected para que otra clase pueda extender de esta, mientras que cerrada significa que nunca modifiques la clase original y padre (en la medida de lo posible).

Sigamos con nuestro ejemplo. El jefe quiere añadir el nombre de su/nuestra empresa en el asunto al enviar el email a un cliente. Tenemos nuestra clase que hasta ahora enviaba los correos electrónicos del sistema :

class EnviarMails {
  public enviarEmail(string $destino, string $asunto, string $mensaje)
  {
    mail($destino, $asunto, $mensaje);
  }
}

Lo primero que se nos podía ocurrir es pensar: "Bueno, pues cambio en la función que envía los mails la parte de $asunto y pongo "Mi empresa: " + $asunto. Arreglado. Nop! Ahora se añadirá el nombre de tu empresa en todos los correos electrónicos que envíe el sistema.

Lo segundo que piensas es: "Vale! Pues un cuarto parámetro y un if: si ese parámetro es true, añado el nombre de mi empresa; si no, no añado nada". Este procedimiento te lo podría comprar si me juras que esto no va a cambiar nunca, pero ambos sabemos que no puedes jurarme eso.

Piénsalo, la nueva funcionalidad que nos han pedido hace prácticamente lo mismo que la que teníamos, solo añade un pequeño aspecto. Si nuestra clase original está más que probada y sabemos a ciencia cierta que funciona, porqué vamos a tocarla. ¡No la toques! Déjala como está. Aquí reside el punto fuerte de la O de SOLID:

  • No toques lo que está probado (y tiene test) y sabes que funciona
  • Al tocar esta clase, cambiarás el comportamiento de TODAS las partes del código que estén usando esta clase y seguramente no es lo que queramos.

A la tercera viene el planteamiento correcto: ¡crearemos una clase nueva que EXTIENDA de la anterior! Es decir, es como copiar (pero sin copiar dos veces el mismo código) la clase original y modificarla a nuestro antojo. Mira nuestra nueva clase:

class EnviarMailsConNombre extends EnviarMails {
  //En algunos lenguajes tienes que añadir una marca de @Override
  // que quiere decir: reemplaza el método enviarEmail de la clase
  // padre por este.
  public enviarEmail(string $destino, string $asunto, string $mensaje)
  {
    $nuevoAsunto = 'Mi empresa:' + $asunto;
    mail($destino, $nuevoAsunto, $mensaje);
  }
}

Si te fijas, EnviarMailsConNombre es una copia de EnviarMails, ya que lo estamos indicando en la cabecera de la clase con extends <ClasePadre>. Así, el compilador o intérprete copiará la clase padre en la nuestra y, cuando llamemos a la función enviarEmail de nuestra clase, hará el cambio de asunto y lo enviará a la clase enviar del padre (que es la que sabemos que funciona).

Conclusión

Vamos a dejar las otras tres patas de SOLID para otra ocasión. Es importante que asumas esto y luego pasemos a lo siguiente.

Hemos aprendido dos patas principales de SO LID. Y las conclusiones que te hago son:

    1. Cada clase, una sola responsabilidad: cada clase, una misión.
    1. Las clases que funcionan, no se tocan ni se cambian. Extiende de estas clases.

Espero que a partir de hoy empieces a usar estos principios y ya verás como te salvan la vida: desarrollador con menos estrés = corazón sin infartos 😉.