Metaprogramación en runtime con Groovy

Una de las peculiaridades que hacen de Groovy un lenguaje tan potente es su capacidad de Metaprogramación. El hecho de que sea un lenguaje activo (opcionalmente como ya vimos) hace que podamos postponer hasta tiempo de ejecución ciertas comprobaciones y resoluciones que en general se harían en tiempo de colección. De esta forma vamos a poder detener, inyectar e inclusive sintetizar nuevas clases y métodos bajo demanda en tiempo de ejecución.

La metaprogramación es una técnica avanzada que esencialmente nos deja redactar código que escribe código. Este género de técnicas, que a priori pueden no representar nada, hacen que podamos solucionar nuestros inconvenientes con un enfoque diferente y a veces la solución puede ser considerablemente más muy elegante, eficaz y conveniente que si utilizamos un enfoque más tradicional.

Si eres desarrollador Java seguramente te vengan a la cabeza reflection, programación orientada a aspectos o bien aun manipulación de bytecode o bien generación de código y creas que esto es algo muy complicado, ¡solamente lejos de la realidad!
En el presente artículo nos centraremos en la Metaprogramación en Runtime de Groovy, vamos a ver qué es, de qué forma y por qué razón marcha en Groovy y vamos a explicar las diferentes técnicas con ejemplos de código a fin de que todo se comprenda mejor.

Un poco de teoría

Hemos comentado que cuando empleamos las distintas técnicas de metaprogramación lo que hacemos es redactar código que escribe código. En la wikipedia tenemos una definición algo más formal:

La metaprogramación consiste en redactar programas que escriben o bien manipulan otros programas (o bien a sí mismos) como datos.

Groovy da estas capacidades de metaprogramación a través del Meta-Object Protocol (MOP).

La clave no es otra que que desde Groovy cuando ejecutamos otro código (bien sea Groovy o bien Java) las llamadas a ese código se mandan a través del MOP. Es en esta capa en la que vamos a poder ejecutar o bien incorporar nuestro código para influenciar o bien alterar la ejecución original.
Otro término esencial que hay que explicar es el de MetaClass que podemos ver como un registro con los métodos y propiedades de cada clase. De esta manera, cuando Groovy va a ejecutar un procedimiento, la primera cosa que hace es procurarlo en este registro y si existe en él, lo ejecutará y no va a llamar al existente en la clase. Esto nos deja incorporar y alterar el comportamiento de una clase si bien no podamos alterar su código fuente.

Técnicas de Metaprogramación

Vamos a contar y explicar con ejemplos las diferentes técnicas que nos ofrece Groovy de metaprogramación en runtime.

Modificar el MetaClass

Como hemos comentado si alteramos el MetaClass asociado a una clase, lo que estamos logrando indirectamente es alterar el comportamiento de esa clase. Veámoslo con un caso.

String.metaClass.saludar = undefined

def nombre = Iván
println nombre.saludar() // Hola Iván!

Estamos alterando la clase java.lang.String que como sabemos es de Java y es final agregando el procedimiento saludar. Realmente lo que hacemos es incorporar al MetaClass ese nuevo procedimiento y Groovy, al buscar ahí primero, lo halla y lo ejecuta.

Aparte de agregar un procedimiento algo que podemos hacer es sobrescribir uno ya existente. Imaginemos el próximo caso:

def rnd = new Random()

tres.times undefined

Random.metaClass.nextInt = undefined
tres.times undefined

La ejecución de este fragmento de código sería:

treinta y ocho millones doscientos setenta y dos mil quinientos ocho
trescientos setenta y cuatro millones setecientos dieciseis mil dieciseis
mil treinta y tres millones cuatrocientos ochenta y tres mil seiscientos veinticinco
cuarenta y dos
cuarenta y dos
cuarenta y dos

Como veis se pueden hacer cosas muy potentes y asimismo malísimas con esto, sed buenos ;)

Categories

El primordial inconveniente de alterar el MetaClass es que los cambios son persistentes y bastante difíciles de deshacer. Además de esto son cambios globales que afectan a todo nuestro código. Una forma mejor de aplicar cambios al MetaClass es emplear Categories. La idea es precisamente igual mas los cambios solo se aplican en el scope de una closure. Una vez ejecutada la closure los cambios al MetaClass se revierten de manera automática.

class Utils undefined

def nombre = Iván
use(Utils) undefined

// Fuera de la closure precedente el procedimiento saludar() no existe. El próximo
// código lanza una groovy.lang.MissingMethodException
// println nombre.saludar()

Extension modules

En ocasiones nos puede resultar útil incorporar el comportamiento deseado al código sin alterarlo explícitamente. Supongamos que pudiésemos agregar un .jar al classpath y de este modo modificasemos ciertas clases conforme nuestras necesidades. Esto lo podemos hacer con los Extension modules.
El truco está en crear un archivo con meta información declarando qué clases deseamos alterar y de qué manera, empaquetarlo todo como un solo jar y sencillamente agregarlo al classpath y usarlo.

package demo
class Utils undefined

# /src/main/resources/META-INF/services/org.codehaus.groovy.runtime.ExtensionModule
moduleName = string-utils-module
moduleVersion = 0.1
extensionClasses = demo.Utils

Uso:

// Para ejecutar:
// ./gradlew build
// groovy -cp build/libs/string-extensions-1.0.jar ExtensionModule.groovy

// extension_module.groovy
@groovy.transform.CompileStatic
class MiClase undefined

def mc = new MiClase()
mc.saluda()

Como veis por el mero hecho agregar al classpath el jar con la clase Utils y el fichero de meta-infomación en el que declaramos el extension module, la clase String tiene el procedimiento saludar().
Las primordiales ventajas de esta técnica es que es compatible con @CompileStatic y que solo hay que agregar una dependencia a nuestro código.

Method missing

Las técnicas que hemos visto hasta el momento pertenecen a la categoría de method injection. Esto quiere decir que agregamos nuevos métodos y propiedades al código cuyo nombre conocemos cuando estamos desarrollando y los inyectamos en el código en tiempo de ejecución.
El empleo de method missing no obstante lo que nos deja es atrapar al vuelo a lo largo de tiempo de ejecución métodos cuyo nombre no conocemos cuando estamos desarrollando el código.

Veámoslo con un caso a fin de que se comprenda mejor. Vamos a incorporar una clase Turismo que contendrá un pequeño DSL para desplazar y parar el vehículo. Nos centraremos en la implementación de los diferentes métodos del DSL y no en el propio DSL en sí.

class Vehículo undefined

def vehículo = new Coche()
println El turismo está parado. Velocidad = dólares americanos undefined

turismo.avanzar(veinte)
println El vehículo avanza a dólares americanos undefined km/h

turismo.acelerar(cincuenta)
println El vehículo acelera hasta los dólares americanos undefined km/h

turismo.parar()
println El vehículo está parado. Velocidad = dólares americanos undefined

//coche.turbo()
// Esto lanzará MissingMethodException

Si ejecutamos el código se va a mostrar lo siguiente:

El turismo está parado. Velocidad = 0
El vehículo avanza a veinte km/h
El vehículo acelera hasta los siete km/h
El vehículo está parado. Velocidad = 0

La clave se encuentra en incorporar en nuestra clase el procedimiento methodMissing que va a ser ejecutado en el momento en que un procedimiento no exista en la clase y en su implementación vamos a poder hacer cuanto queramos. Como veis en un caso así se trata de un caso sencillísimo con una implementación muy básica mas se pueden llegar a hacer cosas muy complejas a priori de una forma parcialmente fácil.

Conclusiones

Hemos visto diferentes técnicas de metaprogramación en runtime que, en función de nuestro caso de empleo, vamos a poder emplear para solucionar un inconveniente concretamente de una forma diferente.
Como comenté al comienzo se trata de técnicas avanzadas que no se acostumbran a emplear en el día tras día sino se deben escoger teniendo presente los pros y los contras de cada caso. Si damos con un buen caso de empleo podemos hacer que nuestra solución sea más muy elegante, eficaz y simple de sostener que con una aproximación más tradicional.

Todos y cada uno de los ejemplos de este blog post están libres en este repo de Github a fin de que podais probar con ellos y comprender mejor los conceptos. Además de esto, para ahondar más en los detalles el apartado sobre metaprogramación en runtime de la página web de Groovy es completísimo.

hljs.initHighlightingOnLoad();

code.hljs undefined
@media only screen and (min-width: 768px) undefined
@media only screen and (min-width: 1024px) undefined

Asimismo te invitamos a

De qué manera explicar el jaleo de los ochenta y uno campeonatos españoles de eSports con un organigrama: primer intento

Mejora tu código Java utilizando Groovy

Kotlin desde la perspectiva de un desarrollador Groovy

– La nueva

Metaprogramación en runtime con Groovy

fue publicada originalmente en

turincon.net