Como usar un compiler pass básico en Symfony

symfony_banner.jpg
Solucionex
25
Dic 20

¿Qué es un compiler pass?

En ocasiones, tenemos que desarrollar una funcionalidad o muchas funcionalidades complejas que dependen de múltiples escenarios y casos donde podemos llegar a crear una estructura comparativa de "if" y "switch" realmente enorme, haciendo que nuestro desarrollo sea nada escalable y difícil de leer y entender. El compiler pass soluciona este problema de escalabilidad y comprensión separando responsabilidades con la misma funcionalidad en distintas clases. 

En definitiva y a groso modo, es una metodología eficiente, fácil de leer y escalable de separar una misma funcionalidad con múltiples condicionantes en distintas responsabilidades.

 

Estructura básica

Un compiler pass en symfony fácilmente se puede separar en bundles si se desea, ya que todo es recogido en una misma paqueteria funcional. 

Su estructura dentro de esta paquetería se basa en:

  • Una interface que implementaran todas las clases
  • un directorio donde alojar todas las clases que implementen la interface, de la cual cada clase se hará responsable de llevar a cabo su trabajo
  • Una clase handle que derivará y hará de semáforo para saber a quien debe delegarle la responsabilidad.
  • Una pequeña configuración en el fichero service.yaml de nuestra aplicación donde definiremos nuestras clases como una paquetería iterable de servicios.

Pero mejor veamos un caso practico de como implementarlo, ya que la teoría puede ser confusa.

 

Ejemplo práctico

Imaginemos una aplicación de una residencia de ancianos donde necesitemos crear un menú en base al día de la semana y la edad. Una manera escueta de hacerlo seria con un servicio que cree estos menús con una serie de if e ifelse donde preguntariamos la edad, el día de la semana, etc. Imaginemos que añadimos medicamentos también. Esto parece fácil, pero en un futuro quizás podríamos tener más condiciones para añadir como tema de alergias o cualquier cosa que se nos ocurra haciendo que cada vez sea más difícil de entender el código y dificulte su escalabilidad, Hay es donde el compiler pass nos soluciona la vida.

 

En primer lugar, hemos creado una paquetería llamada Feeding, ya que se trata de alimentación (y que fácilmente podría ser un bundle):

Feeding

En esta paquetería, podemos ver la interface FeedInterface, la cual implementarán todas las clases dentro del directorio Food, y una clase FeedHandler, que es la que se encargará de derivar las responsabilidades que le entren.

Dentro de nuestro service.yaml de nuestra aplicación Symfony, crearemos la siguiente configuración:

service

En primer lugar creamos un tag para nuestra interface, para posteriormente inyectar un objeto iterable a nuestra clase Handle para que únicamente itere sobre los servicios que aplican la interface mediante el tag creado anteriormente y únicamente a esas clases les transfiera responsabilidades.

Ahora veamos la Interface, las clases que la implementan y el Handler.

Para este caso, en la Interface hemos escrito 2 funciones. La función de range va a ser la encargada de identificar de que clase es responsabilidad aplicar la función addFood. El condicionante va a ser un rango de edad. Es decir, como nuestro criterio para formar el menú va a ser un rango de edad, esta función range va a ser la función que identifique responsabilidades. Para este ejemplo es una condición muy simple, ya que solo comprobamos el rango de edad. Más adelante veremos como implementar estas condiciones.

La función addFood va a ser la encargada de, una vez la clase sepa que tiene que trabajar, de añadir comida al menú.

interface

 

Antes de ver el Handle, veamos como se implementa está interface para comprenderlo mejor.
Dentro del directorio Food, tenemos las clases que implementarán la anterior interface, donde según el rango de edad que entre, añadiran pescado, carne, verduras, postre o pastillas al menú. Veamos como funciona,

Clase Fish:

fish

En primer lugar, definimos una constante donde almacenamos una colección con los rangos de edad donde está permitido que al menú se le añada pescado. Mediante la función range, comprobamos que el rango que entra está dentro del rango de edad permitido y devuelve al Handle un true o false para decirle si es responsable de añadir o no pescado al menú, si entra dentro de ese rango, el Handle (que veremos posteriormente) ejecutará la función addFood, donde según el dia de la semana que sea, añadirá un tipo distinto de pescado.

 

Clase Meat:

meat

Como vemos, la clase Meat añade carne al menú, en este caso vemos que los rangos de edad son distintos.

 

Veamos ahora la clase Handle, la responsable de derivar el trabajo.

handle

Esta es la clase con la que nuestra aplicación se comunicará para crear el menú mediante la función getMenu.

Como podemos ver, el constructor tiene inyectado en sus argumentos un objeto iterable, el cual son las clases que implementan la interface y que hemos configurado en el service.yaml

La función getMenu entra un rango de edad que le enviaremos desde nuestra aplicación, según el rango de edad, iteraremos sobre nuestro objeto iterable y preguntaremos si el rango de edad es permitido dentro de nuestras clases, si es permitido, ejecutará el addFood de dicha clase. Como no hemos puesto un return o cortado la iteración con un break, seguirá iterando en todas las clases preguntando por el rango, ya que en este caso nos interesa que itere todas las clases para añadir comida al menú siempre que esta clase lo permita. En ocasiones habrá veces que solo necesitemos disparar una funcionalidad y fin, en estos casos cortamos la iteración. Una vez construido el menú, devuelve este objeto menú.

 

Como podemos ver, la responsabilidad de ver si un elemento se añade al menú va en función al rango de edad que le entre. Todas las clases tienen la misma funcionalidad pero se implementan de diferente manera, lo cual lo atomiza y hace que sea bastante escalable ya que, si en un futuro deseamos añadir mas elementos al menú, solo tenemos que crear una clase nueva, añadirle su funcionalidad y unas condiciones para que esta funcionalidad se aplique.

 

Conclusión

  • Ventajas
    • atomizar código
    • escalable
    • facil lectura
    • una vez implementado, es rápido extenderle funcionalidad
  • Desventajas
    • al principio, requiere más tiempo de implementación
    • repetición de código
    • podemos llegar a tener una paquetería de clases realmente grande