Programacion metaclases en Groovy

En Groovy es fácil añadir funcionalidad nueva a las clases ya existentes de forma dinámica, usando las metaclases que ofrece Groovy. Las clases a las que ampliamos la funcionalidad pueden ser de Groovy o incluso de Java, sin necesidad de tocarlas. Siempre que las llamemos desde Groovy tendremos acceso a esta nueva funcionalidad.

Una pequeña introducción

Cuando desde Groovy se llama a un método de una clase, por defecto Groovy no llama directamente al método. Siempre llama primero a una metaclase que Groovy añade a todas las clases, tanto a las clases Groovy como a las clases Java. Este metaclase es la que mira si ese método existe realmente en la clase para llamar a dicho método, o dar una excepción si ese método no existe en la clase.

Pues bien, la magia de todo esto es que podemos cambiar esa metaclase por defecto y poner nosotros una a nuestra medida, que haga lo que nosotros digamos. Groovy tiene además otras metaclases que no son las de defecto, como ExpandoMetaclass o ProxyMetClass. Veamos varias posibilidades que nos permite.

Añadir un método estático a una clase

Una metaclase que tiene Groovy es ExpandoMetaClass. Este metaclase permite añadirle métodos y propiedades sobre la marcha, en tiempo de ejecución. Cuando a una clase normal de Java o Groovy le ponemos una ExpandoMetaClass y esa ExpandoMetaClass le añadimos métodos o propiedades, a todos los efectos es como si se los añadiéramos a la clase Java o Groovy original. Para explicar esto un poco mejor, veamos como añadir un método estático a la clase String de Java.

String.metaClass.static.yabadaba = {
   println("doooooo!")
}

String.yabadaba()
// Esto escribe en pantalla doooooo!

Desde Groovy, la clase String de java tiene un atributo metaClass. Ese atributo es la metaclase de la que estamos hablando y tiene la implementación por defecto. Si a esa metaclase le añadimos un método o propiedad, Groovy automáticamente la cambia por una clase ExpandoMetaClass y le añade el atributo o propiedad.

En este ejemplo String.metaClass.static.yabadaba = {...} añade un método static de nombre yabadaba. El código de ese método es la closure que asignemos a ese método, que en este caso sólo saca por pantalla "dooooo!", al más puro estilo Picapiedra. Si la closure devolviera algo, eso sería lo que devolvería el método.

Y esto es todo, sólo nos resta usar el método estático de la forma habitual, String.yabadaba(). Groovy llamará a la metaclase String no tiene ese método, la metaclase sí lo tiene, así que llama al método de la metaclase.

Por supuesto, la clase String sólo tendrá ese método después de que hagamos la asignación. Si intentamos usarlo antes, no lo tendrá y obtendremos un error de que el método no existe.

Añadir un método no estático a una clase

Con el mismo ejemplo anterior, si quitamos static, el método será un método no estático. Tendremos que tener una instancia de String para poder llamarlo. Veamos el ejemplo

String.metaClass.print = { println delegate }

"Hello".print()
// Esto saca en pantalla Hello

Le hemos añadido en esta ocasión un método print() que saca el String por pantalla. Dentro de la closure, delegate es el String en cuestión. Si es necesario, podríamos llamar a los métodos del String a través de delegate, por ejemplo delegate.charAt(2) nos daría el caracter de índice 2 del String.

Y ya podemos usarlo, cualquier instancia de String, la hayamos creado antes o la creemos nueva a partir de ahora, tendrá un método print() para sacar el String por pantalla.

Añadir un método a una instancia concreta

Hasta ahora hemos usado el nombre de la clase y metaClass. Esto afecta a todas las instancias de la clase, las hayamos creado antes o las creemos después, a partir del momento en que hagamos esa asignación. Pero podemos cambiar la metaclase de un objeto/instancia concreto, no de la clase en sí misma. El siguiente código nos muestra cómo hacerlo

String hello = "Hello You"

hello.metaClass.yourName = {String name -> return delegate + " " + name }

println hello.yourName("Peter")
// Saca por pantalla "Hello You Peter"

Simplemente creamos un objeto en este caso de la clase String "Hello You" y lo guardamos en la variable hello. Basta usar la metaclass de esa variable hello como hemos hecho hasta ahora con la clase String. Llamamos a metaClass y le ponemos un método yourName

Solo este objeto String tendrá ese método, todos los demás String que creemos antes o después, no lo tendrán.

Capturar llamadas a métodos no existentes

El siguiente comportamiento es por defecto, no estamos haciendo ningún cambio en la metaclase por defecto. La metaclase por defecto tiene un método methodMissing() al que podemos pasar como parámetro una closure. Cuando alguien llame en una clase a un método que no exista, la metaclase llamará a la closure que le pasamos a través de methodMissing(). Y ahí nosotros podemos controlar qué hacer con esa llamada.

Veamos un ejemplo

String.metaClass.methodMissing ({ String name, theArgs ->
   println "${name}(${theArgs}) no existe  :("
   return "No toy"
})

println "Hello".yeeee("haaaa")

// Saca por pantalla
// yeeee([haaaa]) no existe  :(
// No toy

Hemos pasado una closure que recibirá dos parámetros: el nombre del método name y una lista con los parámetros que se pasen al método cuando se le llamen theArgs. Y en esa closure sólo hemos metido una salida por pantalla indicando que el método con esos parámetros no existe.

Cuando llamemos a un método de String que no exista, como "Hello".yeeee("haaaa"), se llamará a nuestra closure y lo que devuelva nuestra closure será lo que devuelva el método.

Proxy para capturar todas las llamadas a una clase

Podemos aprovechar este mecanismo para interceptar todas las llamadas a métodos que se hagan a una clase. Y en el código que intercepta podemos hacer lo que queramos, como cambiar los valores de los parámetros que nos han pasado, cambiar el valor que devuelve el método, o incluso impedir la llamada al método de la clase original.

El mecanismo es algo más complejo, vamos por partes. Imagina que tenemos una clase cualquiera cuyos métodos queremos interceptar. La clase para este ejemplo será una simple calculadora con un método para sumar dos enteros

class MyCalculator {
    public int add(int a, int b){
        a+b
    }
}

Debemos ahora escribir la clase interceptora, la clase que contiene el código que queremos que se ejecute cada vez que alguien llame a un método de MyCalculator. Esta clase interceptora debe implementar la interfaz Interceptor. Puede ser como la siguiente

class CalculatorInterceptor implements Interceptor {

    @Override
    Object beforeInvoke(Object object, String methodName, Object[] arguments) {
        println("beforeInvoke : ${methodName}(${arguments})")
        if (arguments.size()>0){
            arguments[0]=33
        }
    }

    @Override
    Object afterInvoke(Object object, String methodName, Object[] arguments, Object result) {
        println("afterInvoke : ${methodName}(${arguments})")
        result
    }

    @Override
    boolean doInvoke() {
        println("doInvoke")
        true
    }
}

Veamos el significado de todos esos métodos y cómo funcionan. Cuando se llama a un método de la clase que está siendo interceptado por esta clase (ya veremos como se configura para que sea interceptado)

  • Primero Groovy llama a beforeInvoke, pasando como parámetros la instancia de la clase a la que se está llamando (en nuestro ejemplo será una instancia de MyCalculator, el nombre del método al que se está llamando, en nuestro caso será add y un array con los parámetros que se están usando para llamar al método, en nuestro caso será un array con un par de elementos que serán un entero cada uno. Si devolvemos algo en el método beforeInvoke, será lo que devuelva la llamada al método myCalculator.add(), siempre y cuando los demás metodos del inerceptor que van a ser llamados después respeten este valor.
  • Después se llama a doInvoke(). Si esta llamada devuelve false, no se hará la llamada a myCalculator.add() y lo que haya devuelto el método beforeInvoke() sigue valiendo como posible valor de respuesta a myCalculator.add(). Si doInvoke devuelve true, se hará la llamada a myCalculator.add() y lo que devuelva esta llamada reemplazará a lo que devolvimos en beforeInvoke.
  • Finalmente se llama afterInvoke, pasando como parámetros la instancia que estamos interceptando (myCalculator), el nombre del método, la lista de parámetros y el resultado que ha devuelto la llamada al método (o lo que devolvió beforeInvoke si doInvoke dijo que no se hiciera la llamada al método. Lo que devolvamos en este método (habitualmente el parámetro result), será lo que finalmente devuelva el método myCalculator.add().

La siguiente imagen ilustra todo este flujo

diagrama de llamadas a un interceptor de groovy

Ya sólo nos queda enganchar todo, con el siguiente código

def proxy = ProxyMetaClass.getInstance(MyCalculator)
proxy.interceptor = new CalculatorInterceptor()
MyCalculator.metaClass = proxy

MyCalculator calculator = new MyCalculator()
println("Invoking...." + calculator.add(-4,-5))
  • En la primera línea creamos un proxy para la clase MyCalculator
  • En la siguiente línea asignamos nuestro interceptor al proxy
  • En la tercera línea usamos el proxy como metaclass de la clase MyCalculator.

Listo, todo enganchado, a partir de ahora todas las instancias de MyCalculator, ya creadas o creadas a partir de ahora, tienen el proxy y capturaremos con él todas las llamadas a los métodos de la clase.

Añadir los métodos temporalmente

Existe otra forma de añadir métodos a clases ya existentes sin tocarlas. Se llaman Category y es una forma de añadir temporalmente métodos adicionales a una clase. Veamos cómo hacerlo.

Primero, tenemos que crear una clase Category. Esta clase sólo debe cumplir una serie de criterios para poder ser usada como tal. Los criterios es que debe tener métodos estáticos cuyo primer parámetro sea la clase a la que queremos añadir funcionalidad. Por ejemplo, si queremos añadir métodos a la clase String de java, el primer parámetro de estos métodos debe ser de tipo String. Por ejemplo

class StringCategory {
    static String shout(String self){
        self.toUpperCase()
    }
}

Hemos llamado a la clase StringCategory porque es la nomenclatura que usa groovy, pero podemos poner el nombre que queramos. Esta clase tiene un método estático cuyo primer parámetro es String y para probar, simplemente devuelve la cadena en mayúsculas.

Para usar esta clase, el código sería el siguiente

use(StringCategory){
   println "Hello".shout()
}

Esto saca por pantalla "HELLO". Hemos puesto use y entre paréntesis nuestra clase category. El código que va entre llaves tendrá String con el método shout() disponible, pero sólo entre las llaves. Es por eso que este funcionalidad añadida es temporal, sólo es posible si se pone dentro de un use(StringCategory)

Enlaces