Pereza I: Meta-programación
La meta-programación es mágica. Esa es la razón principal por la que no deberíamos usarla. Hay muchas terribles consecuencias en el horizonte.
Con la meta-programación pasa lo mismo que con los patrones de diseño:
Existen estados de excitación por los que todos los programadores pasamos. ¿En qué estado estás tú?
- Los conocemos.
- No los entendemos.
- Los estudiamos a fondo.
- Los dominamos.
- Leemos la biblia que nos dice que los patrones están en todos lados.
- Abusamos de ellos pensando que son una bala de plata.
- Aprendemos a evitarlos.
La magia está al alcance de la mano
Cuándo usamos meta-programación lo hacemos predicando sobre el Metalenguaje y el Meta Modelo. Esto implica subir un nivel de abstracción al hablar por sobre los objetos del dominio del problema.
Esta capa extra nos permite razonar y reflexionar sobre la relación entre los entes de la realidad en un lenguaje de más alto nivel.
Al hacerlo rompemos la biyección con la que observamos la realidad dado que en el mundo real no existen modelos ni meta modelos, solo entidades del negocio que estamos modelando.
Cuando estamos atacando un problema de negocio en la vida real es muy difícil que justifiquemos referencias a meta-entidades.
Dichas meta-entidades no existen con lo cual no nos mantenemos fieles a la única regla de la biyección entre nuestros objetos y la realidad.
Por lo tanto, sería muy difícil para nosotros justificar la existencia de objetos extra y responsabilidades inexistentes en el mundo real.
A los programadores nos gusta reflexionar sobre nuestros modelos. Esto es una buena práctica porque nos permite buscar generalizaciones, siempre y cuando existan en dicha realidad.
Abierto pero no tanto
Uno de los principios de diseño más importante es el llamado abierto/cerrado perteneciente a la definición de diseño sólido. La regla de oro indica que un modelo debe ser abierto para la extensión y cerrado para la modificación.
Esta regla sigue siendo verdad y es algo que deberíamos tratar de enfatizar sobre nuestros modelos. Sin embargo, en muchas implementaciones encontramos que la forma de hacer que estos modelos sean abiertos es dejar la puerta abierta, utilizando sub-clasificación.
Como implementación de extensión el mecanismo parece a simple vista muy robusto y muy bueno pero nos genera el único problema que podemos tener en el desarrollo de software:
Al vincular la definición que indica dónde obtener los posibles casos, aparece una inocente referencia a una clase y a sus subclases, que es la parte que podría cambiar dinámicamente (La extensión buscada).
El algoritmo utilizado es pedirle a la clase Parser que interprete cierto contenido. La solución consiste en intentar delegar la interpretación del contenido a cada una de sus subclases hasta que alguna de ellas acepte que puede interpretarlo y se encargue de proseguir, asumiendo dicha responsabilidad.
Este mecanismo es un caso particular del patrón de diseño Cadena de Responsabilidades.
Sin embargo tiene varios problemas:
- Genera acoplamiento a la clase Parser que es el punto de entrada de dicha responsabilidad.
- Utiliza a las subclases con meta-programación, por lo tanto, al no existir referencias directas, no serán evidentes sus usos y referencias.
- Al no existir referencias y usos no se podrán realizar refactorizaciones directas, conocer todos sus usos ni evitar borrados accidentales.
Este problema enunciado es común a todos los frameworks de caja blanca, siendo el más conocido y popular de ellos la familia xUnit y todos sus derivados.
Las clases son variables globales y, por lo tanto generan el acoplamiento y no es la mejor forma de abrir un modelo.
Veamos un ejemplo de cómo se puede abrir de manera declarativa utilizando el principio Open/Closed:
- Desacoplamos la referencia a Parser.
- Generamos una dependencia a un proveedor de parsing utilizando Inyección de Dependencias (la D de Solid).
- En distintos ambientes (producción, testing, configuraciones) utilizamos distintos proveedores de Parsing y no nos acoplamos a que pertenezcan a una misma jerarquía.
- Utilizamos acoplamiento declarativo. Le pedimos a dichos proveedores que realicen la interfaz ParseHandling.
Sin referencias no hay evolución del código
El problema más grave que tenemos al utilizar meta programación consiste en tener referencias oscuras hacia las clases y métodos que nos va a impedir todo tipo de refactorizaciones y por lo tanto nos van a impedir que el código crezca, únicamente evitable en el utópico escenario de tener 100 % de cobertura de tests.
Al perdernos la cobertura de todos los casos posibles vamos a estar omitiendo un posible caso que es referenciado de una manera indirecta y oscura. Este no va a ser alcanzado por nuestras búsquedas y, refactorizaciones de código generando errores indetectables en producción.
El código debería ser limpio, transparente y tener la menor cantidad posible de meta referencias que no sean alcanzadas por alguien que pueda modificar dicho código.
Veamos un ejemplo de una construcción dinámica de un método:
Si estamos en un cliente configurado en español la llamada de arriba invocará el método getLanguageEs();
El problema que genera este referencia oscura es el mencionado en el ejemplo del parser. Dicho método no tiene referencias, no puede refactorizarse, no se puede determinar quiénes lo utilizan, cuál es su cobertura, etc. En estos casos, con una dependencia explícita (aunque sea utilizando tablas de mapeo), nos evitamos todos los conflictos y la magia negra de la meta-programación.
La excepción define a la regla
Ya hemos demostrado que utilizar meta programación para referirse a nuestras entidades del mundo real violando el principio de la biyección es una mala práctica. Entonces ¿Para qué deberíamos usarla ?
Cuando realizamos una simulación, debemos mantenernos lo más lejos posible de aspectos accidentales del modelado que no hacen al negocio. Entre dichos aspectos están:
- La persistencia
- La serialización de entidades
- La impresión o ‘display’ en interfaces de usuario
- El testing o las aserciones
Dichos problemas pertenecen al dominio ortogonal del modelo computable y no son particulares a ningún negocio. Interferir con las responsabilidades de un objeto es una violación a su contrato y su protocolo. En en vez de agregar ‘capas accidentales’ de responsabilidades podemos hacerlo utilizando meta-programación.
No es mi responsabilidad
Imaginemos como modelar un ente de la vida real ‘Empleado’:
Si nos mantenemos fieles a nuestra única regla de diseño las dos primeras funciones son esenciales al dominio del problema y las últimas no pertenecen a las responsabilidades del negocio.
Por lo tanto deben desaparecer:
Utilizar meta-programación para satisfacer dichas responsabilidades accidentales para acceder a dichos objetos asegura la pureza de su protocolo.
Sin embargo trae problemas de acoplamiento y de violación de encapsulamiento. Algunos lenguajes incorporan el concepto de Friend Class que puede ser utilizado en lugar de la meta-programación.
Distinguir las responsabilidades accidentales de las esenciales es el mayor desafío para un desarrollador de software.
La excepción a la excepción
La mayoría de los los frameworks de testing usan técnicas de metaprogramación para recolectar los elementos a testear o casos de test
Por ejemplo: xUnit busca todas las subclases de TestCase y todas las funciones que comiencen con testXXX para armar los casos de prueba.
Al haber realizado muchas pruebas unitarias, tarde o temprano, surge la necesidad de testear un método privado de un ente del dominio de problema
Ante dicha situación existen dos posibilidades:
- Hacer público el método anteriormente privado.
- Utilizar meta programación para ‘invocar’ un método privado con algún mecanismo de reflexión que evite los controles.
Pero para hacer 1) deberíamos empezar a exponer comportamiento accidental que no pertenece al ente real violando la regla de la biyección y con respecto a 2) muchos lenguajes no lo permiten y además es una mala práctica de diseño
Para resolver el dilema podemos consultar el sitio shoulditestprivatemethods.com creado por el gran Kent Beck.
La alternativa es pensar cuál es la razón de querer testear una función privada. La respuesta siempre es que la función privada hace un cálculo interno o modela un algoritmo.
Para resolver el entuerto dejamos el método privado. Extraemos el algoritmo y lo testeamos unitariamente, logrando reificación del concepto (que seguramente existe en el dominio del problema) y su posible reutilización favoreciendo la composición. Final feliz.
¡¡ y lo podemos testear !!
De la misma manera que lo realizado en los tests, La serialización y la persistencia pueden utilizarse con meta-programación porque no son problemas reales del dominio que estamos modelando y no deberían agregar “comportamiento extra” ensuciando los objetos.
Más pereza
Lamentablemente, la meta programación no es el único mal generado por la pereza.
En este artículo mostraremos otro problema:
Conclusiones
La meta-programación es algo que deberíamos evitar a toda costa, utilizando en su lugar abstracciones que existen en la realidad y que sólo debemos ir a buscar pacientemente.
La búsqueda de dichas abstracciones nos demanda un conocimiento del dominio bastante más profundo de lo que salimos obtener. Los programadores, en general por pereza, solemos inventar artilugios y mecanismos ante la necesidad de aprender un dominio nuevo, en vez de ir a buscarlos a la realidad.
Parte del objetivo de esta serie de artículos es generar debates y espacios de discusión sobre la problemática del diseño de software.
Esperamos ansiosamente los observaciones y comentarios sobre esta nota.
Este artículo fue publicado al mismo tiempo en inglés aquí.