Introducción a Groovy (V)

En esta quinta entrega del tutorial de Groovy vamos a ver el concepto de metaprogramación. Mediante metaprogramación podemos escribir código que genera o modifica otro código (por ejemplo otros programas) o incluso a si mismos (código que tiene la habilidad de cambiar su comportamiento en tiempo de ejecución). Esto nos permite, entre otras cosas, manejar situaciones que no estaban previstas cuando se escribió el código, y sin necesidad de recompilar.

5.1 Un poco de reflexión

La metaprogramación en Groovy, al igual que en Java, se basa en la capacidad del lenguaje llamada reflexión. Mediante reflexión podemos (entre otras cosas) conocer los miembros de una clase:

String.interfaces.each { println it }
String.constructors.each { println it }
String.methods.each { println it }
	

Cada una de las lineas del código anterior muestran los interfaces que implementa una clase, sus constructores y sus métodos, respectivamente. Para los dos últimos casos, se muestran solo los miembros públicos (public). Así mismo, todo objeto en Groovy tiene un método getClass() que devuelve el objeto Class asociado a su clase. Recuerda que podemos invocar la propiedad de una clase mediante su metodo getter, o directamente por su nombre (el getter es llamado en este caso implícitamente):

println String.class
	

Y hablando de propiedades, veamos como mostrar todas las propiedades de un objeto:

def s = new String("cadena de texto")
s.properties.each { propiedad ->
    println propiedad
} 
	

El ejemplo anterior nos muestra que todo objeto String contiene tres propiedades: class (la cual vimos en acción en el ejemplo anterior), bytes (un array de bytes que contiene el valor del String) y empty que como su nombre indica contiene un valor true en caso de que la cadena de texto tenga longitud cero y false en caso contrario. Ten presente que todas las propiedades de un objeto son deducidas por medio sus métodos getter/setter.

5.2 MetaClass

Todas las clases Groovy disponen de un miembro llamado metaClass. Este miembro nos permite, entre otras cosas, comprobar si la clase dispone de una propiedad concreta gracias a su método hasProperty():

class Articulo {
    String descripcion
    double precio
}

def boligrafo = new Articulo(descripcion:"Boligrafo negro", precio:0.45)
if(boligrafo.metaClass.hasProperty(boligrafo, "precio")) {
    // ...
} 
	

Así mismo, también podemos comprobar la existencia de un método con respondsTo():

if(boligrafo.metaClass.respondsTo(boligrafo, "getDescripcion")) {
    // hacer algo
} 
	

5.3 Ejecutando un método mediante GString's

Una manera alternativa de invocar un método dinámicamente es construyendo la llamada con un objeto GString:

def nombreDelMetodo = "getPrecio"
boligrafo."${nombreDelMetodo}"() 
	

En la llamada al código anterior, cuando se ejecute el programa se sustituirá la expresión ${nombreDelMetodo} por su valor, produciendo así la llamada a getPrecio() en el objeto boligrafo.

5.4 Punteros

En el cuarto artículo del tutorial vimos como acceder directamente al valor de una variable (a una propiedad) mediante el operador @. Este acceso directo se conoce como puntero:

println boligrafo.@precio
	

De manera parecida, mediante el operador & podemos crear un puntero a un método:

def lista = []
def insertar = lista.&add
insertar "valor1"
insertar "valor2" 
	

En el código anterior, hemos creado un puntero (o alias) al método add() del objeto lista y lo hemos asignado a la variable insertar. Desde ese momento, podemos ejecutar comandos como insertar "valor1" y sería equivalente a ejecutar lista.add("valor1"). Esto puede ser muy intuitivo de usar para alguien que no entiende sobre programación en general y Groovy en particular. Esto es, hemos creado un DSL (Domain Specific Language, Lenguaje Especifico de Dominio). Un DSL es un lenguaje que se adapta al dominio al cual se refiere y aplica (por ejemplo el lenguaje jurídico para la jurisprudencia), etc. Así podemos crear un lenguaje fácil de usar, ya que su sintaxis refleja la terminología del problema que se quiere resolver.

5.5 Expando

Un objeto Expando es como un objeto en blanco, al cual podemos añadir métodos y propiedades a la carta, y además de la forma más sencilla posible: dándoles un valor. Veamos un ejemplo:

def posicion = new Expando()
posicion.latitud = 15.47
posicion.longitud = -3.11 
	

En el código anterior hemos definido un objeto Expando llamado posicion, al cual le hemos proporcionado un valor para cada una de las propiedades latitud y longitud (propiedades que al no existir anteriormente para este Expando han sido creadas automáticamente). Dichas propiedades pueden ser después leídas como en un objeto normal. Pero vayamos un poquito más allá, vamos a definir un método para nuestro expando:

posicion.diferencia = { nuevaLatitud, nuevaLongitud ->
    """
    Existe una diferencia de:
        LATITUD: ${posicion.latitud - nuevaLatitud}
        LONGITUD: ${posicion.longitud - nuevaLongitud}
    
    con respecto al la posición anterior: ${posicion.latitud} ${posicion.longitud}
    """
} 
	

Para definir un método simplemente tenemos que definir su nombre (evidentemente) y asignarle un closure. En el código anterior, dicho closure contiene un Heredoc donde se realizan algunos cálculos con las variables pasadas como argumento y las que ya contenía el Expando, devolviéndose finalmente una cadena de texto (recuerda que los Heredoc son cadenas de texto multilínea, y que al ser la última (y única) sentencia del closure es también su valor de retorno implícito). Ahora, podemos llamar al siguiente código:

posicion.diferencia(10.0, 1.0)
	

Como puedes ver, Expando es una herramienta tremendamente potente para crear nuevos objetos, y, como veremos un poco más adelante en este mismo artículo, para ayudarnos en nuestro camino a través de la metaprogramación.

5.6 Propiedades dinámicas

Aunque la clase Expando nos permite añadir propiedades de manera dinámica a un objeto de su mismo tipo, ¿que ocurre cuando queremos disponer de dicho dinamismo en nuestras propias clases? Groovy, como siempre, al rescate:

class Articulo {
    String descripcion
    double precio
    def propiedades = [:]
    
    void setProperty(String nombre, Object valor) {
        propiedades[nombre] = valor
    }
    
    Object getProperty(String nombre) {
        propiedades[nombre]
    }
} 
	

Groovy nos permite definir los métodos getProperty() y setProperty(), y el truco consiste en almacenar los valores pasados a setProperty() en un objeto Map (al que hemos llamado propiedades) para poder ser después leídos. Con esta infraestructura, ya podemos leer y escribir nuevas propiedades dinámicamente en nuestra clase Articulo:

def articulo = new Articulo()
articulo.codigoEAN = 84123445593
println articulo.codigoEAN
	

Cuando nuestro código llama a getCodigoEAN() la llamada es 'redirigida' internamente mediante getProperty('codigoEAN'), con total transparencia para nosotros y sin necesidad de que, en tiempo de compilación, exista esta propiedad ni sus correspondientes getters/setters. Un efecto secundario de definir propiedades dinámicas para una clase es que la lectura de una propiedad aún no definida (y por tanto inexistente) devolverá un valor null, en lugar de lanzar una excepción (un comportamiento en cierto modo parecido al producido por el operador de referenciación segura, como vimos en el punto 1.14, pero aplicado a propiedades en lugar de a objetos).

5.7 Interceptando llamadas a métodos

Groovy nos permite interceptar llamadas a métodos que no existen:

class Articulo {
    String descripcion
    double precio
    
    Object invokeMethod(String nombre, Object args) {
        println "Invocado método ${nombre}() con los argumentos ${args}"
    }
}

def articulo = new Articulo()
articulo.operacionInexistente('abc', 123, true) 
	

En la clase anterior hemos definido el método invokeMethod(), el cual interceptará las llamadas a cualquier método no definido. Un efecto secundario de este comportamiento es que en tiempo de ejecución no se producirá una excepción informando de la no-existencia de dicho método. Sin embargo, la finalidad de invokeMethod() no es evitar este tipo de errores (ni debería serlo) si no, por ejemplo, la construcción de clases con un comportamiento totalmente dinámico, como constructores de XML y HTML (en los que los nombres de los métodos que pasemos pueden ser usados para darles nombre a los nodos XML-HTML del documento que estamos construyendo, y el cuerpo de cada método ser interpretado como un subnodo que será procesado de la misma manera).

Si lo que deseamos es interceptar las llamadas a métodos que si existen, además de definir el método invokeMethod() nuestra clase debe implementar la interface GroovyInterceptable. Dentro de invokeMethod() tenemos que obtener el método interceptado (a través de su metamétodo) e invocarlo:

def invokeMethod(String nombre, args) {    
	def metaMetodo = Articulo.metaClass.getMetaMethod(nombre, args)
	metaMetodo.invoke(this, args)
} 
	

Esto nos permite realizar algunas cosas interesantes, como implementar un around-advice al mas puro estilo Programación Orientada a Aspectos:

class SimpleLogger {
    def logInicio(String metodo, args) {
        println "Iniciado el método ${metodo}() con los argumentos ${args}"
    }
    
    def logFin(String metodo, resultado) {
        println "$Finalizando el método ${metodo}() con el resultado ${resultado}"
    }
}

class Articulo implements GroovyInterceptable {
    String descripcion
    double precio
    SimpleLogger logger
    
    void añadirDescuento(double porcentajeDescuento){
        precio = (precio * (100 - porcentajeDescuento) / 100)
    }

    def invokeMethod(String nombre, args) {    
        logger.logInicio(nombre, args)
        def metaMetodo = Articulo.metaClass.getMetaMethod(nombre, args)
        def resultado = metaMetodo.invoke(this, args)
        logger.logFin(nombre, resultado)
    }
}

def logger = new SimpleLogger()
def articulo = new Articulo(descripcion:'Libreta', precio:1.40)
articulo.logger = logger

articulo.añadirDescuento(15)
println articulo.precio 
	

El código anterior define una sencilla clase de logging con dos métodos que muestran por pantalla información sobre el inicio o fin de un método. Ambos métodos son llamados en la primera y última linea dentro de invokeMethod(), de manera que cada vez que llamemos a un método de la clase Articulo se mostrará el mensaje correspondiente a su inicio y fin. Un tercera opción sería disponer de un método para loggear cualquier excepción que se produzca, pero este pequeño ejercicio lo dejo para tí. Una pregunta que podrías estar planteandote ahora es: ¿qué ocurre si, con el código anterior, invocamos un método que no existe (como en nuestro primer ejemplo de este punto)? Pues que la variable metaMetodo contendrá un valor null (ya que getMetaMethod() no va a encontrar dicho método al no existir), y por tanto metaMetodo.invoke(this, args) producirá una excepción de tipo NullPointerException. La manera de evitar esto es tan simple como hacer uso del operador de referenciación segura:

def resultado = metaMetodo?.invoke(this, args)
	

Con la inclusion del operador de referenciación segura nuestra pequeña clase es capaz de interceptar las llamadas a TODOS sus métodos, existan o no:

articulo.noExiste()
	

5.8 Categorías

Groovy permite la adición de nueva funcionalidad a una clase de la que, por ejemplo, no disponemos del código fuente. Esto es posible a traves de una categoría, la cual implementa los métodos que deseamos añadir a dicha clase. Veamos un ejemplo aplicado a Articulo:

class Articulo {
    String descripcion
    double precio
}

class ArticuloExtras {
    static double conImpuestos(Articulo articulo) {
        return articulo.precio * 1.18
    }
}

Articulo articulo = new Articulo(descripcion:'Grapadora', precio:4.50)
use(ArticuloExtras) {
    articulo.conImpuestos()
} 
	

En el código anterior definimos una categoría llamada ArticuloExtras que define un método extra para la clase Articulo. Los métodos que deseamos añadir deben seguir la siguiente estructura:

static tipoDeRetorno nombre(ClaseDeDestino, argumento1, argumento2, ...) 
	

Finalmente, aplicamos la categoría dentro de un bloque use, el cual requiere como parámetro el nombre de la categoría a usar. También podemos indicar más de una categoría dentro de un bloque use, separando sus nombres con comas. Recuerda que los nuevos métodos solo estarán disponibles dentro del bloque use.

5.9 ExpandoMetaClass

El concepto de ExpandoMetaClass combina los conceptos de metaClass y Expando, permitiendo así añadir métodos a cualquier clase. El concepto es el mismo que ya hemos aplicado con las Categorias, solo que esta vez los métodos están disponibles a nivel global, sin necesidad de ceñirnos a su uso dentro de un bloque concreto. Un ejemplo típico del uso de ExpandoMetaClass es el siguiente:

Integer.metaClass.numeroAleatorio = {
    def random = new Random()
    random.nextInt(delegate.intValue())
}

50.numeroAleatorio() 
	

En el código anterior añadimos el método numeroAleatorio() al objeto metaClass de la clase Integer, asignandole un closure que contiene el cuerpo del método. La primera línea dentro del closure define un objeto Random y lo asigna a una variable. Es en la segunda línea de código donde se genera la magia: llamamos al método Random.nextInt() pasándole como parámetro el propio objeto donde se está realizando la llamada a numeroAleatorio(), esto es, delegate (en este caso concreto será una instancia de Integer), y a continuación se llama a su método intValue() que devuelve un int en lugar del Integer original. Puesto que la capacidad de Java de hacer autoboxing y autounboxing tambíen está disponible en Groovy, podemos escribir el código de la siguiente manera:

random.nextInt(delegate)
	

Lo importante es que tengas presente que delegate hace referencia al objeto 'delegado', el objeto que estará disponible en tiempo de ejecución y desde el que se ejecutará el bloque de código con el que estamos tratando. Otro objeto que podemos alcanzar desde dentro del closure es el propio closure, en este caso con la palabra this.

Hay todavía mucho que decir sobre metaprogramación, sobre todo ver ejemplos más complejos que los aquí mostrados, pero todos los temas están tratados y dejo en tu mano profundizar más en el tema. La metaprogramación puede ser dificil de entender para los que vienen de lenguajes estáticos como Java, donde apenas se hace uso de las librerias de reflexión (si alguien las usa habitualmente que me perdone por ese comentario, pero no es lo habitual) y todo se deja más bien atado en tiempo de compilación. Sea como sea, la metaprogramación nos permite hacer cosas realmente increibles, y nunca está de más tener unos conocimientos básicos sobre ella.

En el próximo capítulo de este tutorial se verá como usar el lenguaje para tareas cotidianas, como parsear y generar XML, leer y escribir archivos, y algunos temas más; en resumen, un 'manos a la obra' con todo lo que hemos aprendido hasta ahora y algunas cositas nuevas.