DSL en Groovy parte II : De estatico a dinamico

Antes que otra cosa, debo aclarar que este, como el post anterior sobre DSL, no prentende ser un tutorial completo de como crear un DSL como gradle; mas bien, intenta dar una mirada y explicar como comenzar para entonces SI, llegar a crear un DSL.

Comenzare con el siguiente ejemplo :

Integer.metaClass.toString = {
    return "El numero es " + delegate
}

println 5.toString()

Tal vez algunos pregunten "y por que querria hacer esto con un Integer si ya cuenta con un metodo toString() ?", la respuesta seria: "no se", pero sirve como ejemplo para resolver otros problemas o necesidades sobre clases (incluso finales como String) del JDK.

Ahora, "¿la sobreescritura de un método del JDK tiene efecto desde todas las clases, o solo durante el código donde se ejecuta?", la respuesta es: si, tiene efecto en toda la máquina virtual, pero solo dentro del código de las clases de Groovy, nunca dentro de una clase Java.

Sabemos que hay un poco de “magia”, la que hace que Groovy pueda acceder a variables y métodos que todavía no han sido creados en tiempo de compilación, pero que sabemos que posteriormente existirán en tiempo de ejecución. Cuando decimos que Groovy es dinámico o hablamos de Duck-typing, estamos hablando sin querer de esta “magia”.

¿Y cómo funciona realmente esta “magia”? ¿cómo hace Groovy para que, generando código 100% Java (que sabemos que tiene tipado estático) se convierta en tipado dinámico, cuando Java no lo es? Todas las clases compiladas y generadas por Groovy, y todas las que se usan desde Groovy, implementan siempre groovy.lang.GroovyObject, por lo que poseen los siguientes métodos:

Object invokeMethod(String name, Object args);
Object getProperty(String propertyName);
void setProperty(String propertyName, Object newValue);
MetaClass getMetaClass();
void setMetaClass(MetaClass metaClass);

Los tres primeros métodos se encargan de interceptar todos los accesos de escritura y lectura de atributos de la clase, y de interceptar todas las llamadas a cualquier método que pueda poseer la clase, sin verificar que exista o no.

La implementación de invokeMethod, por ejemplo, dependerá de muchos factores, pero podría ser la siguiente: usará reflexión para averiguar si la clase actual tiene un método con ese nombre que acepte los parámetros enviados (y con aceptar me refiero a número y tipo) y ejecutarlo; y si no lo encuentra, buscará un atributo de tipo Closure con ese nombre e intentará ejecutarlo, etc. Todas estas búsquedas sobre que método ejecutar (o qué atributo leer o escribir) se producen en tiempo de ejecución, produciendo el efecto de “código dinámico”, es decir, código cuyo comportamiento puede ser modificado en tiempo de ejecución.

Después hay otros dos métodos: getMetaClass() y setMetaClass() que nos permiten acceder (y modificar) el MetaClass relacionado con el objeto actual. El MetaClass contiene todos los atributos y métodos dinámicos que hemos añadido a la clase.

Y retomando el codigo inicial, lo único que hacemos es añadir un closure en el objeto MetaClass de la clase Integer, de manera que cuando llamamos a nuestro método toString(), lo intercepte su MetaClass, encuentre el que acabamos de añadir y lo ejecute en vez de invocar al método toString() original del JDK. Por esta razón, esto solo funciona si la clase que ejecuta el toString() es una clase Groovy, no una Java.

Ahora, pensemos en algo asi

println 26.days.ago.at(8:26)

Insisto, esto no pretende ser un ejemplo completo de DSL, pero si, una explicacion ligera de como comenzar uno.

Que debemos hacer para que esto sea posible ? Modificando el metaclass de la clase Integer y la clase Calendar de la siguiente forma :

Integer.metaClass.getDays = { ->
  delegate
}

Este codigo, lo unico que hace es regresar el delegado, que en nuestro ejemplo, es una instancia de Integer con valor 26 (recordando que la ultima linea de codigo de un metodo o closure es equivalente a return).

El siguiente metodo a agregar al metaclass de la clase Integer es getAgo() de la siguiente forma :

Integer.metaClass.getAgo = { ->
    def date = Calendar.instance
    date.add(Calendar.DAY_OF_MONTH, -delegate)
    date
}

Este metodo, lo que hace es tomar el delgado (en este caso, la instancia de Integer con valor 26) y crear un Calendar, al cual, le restara a los dias del mes, la cantidad que diga este delegado; finalmente regresara la instancia de Calendar creada y modificada.

Finalmente, como este metodo agregado al metaclass nos devuelve un Calendar, debemos agregar el metodo at:

Calendar.metaClass.at = { Map time ->
    def hour = 0
    def minute = 0
    time.each {key, value -> hour = key.toInteger()
      minute = value.toInteger()
      }
    delegate.set(Calendar.HOUR_OF_DAY, hour)
    delegate.set(Calendar.MINUTE, minute)
    delegate.set(Calendar.SECOND, 0)
    delegate.time
}

El metodo at, toma un mapa, por que ? por que en groovy un mapa es de la forma llave:valor, en este caso 8:26 es un mapa con llave 8 y valor 26, y esa es la razon por la que itera por cada elemento del mapa (en nuestro ejemplo solo tiene un elemento) y obtiene la llave como hora y el valor como minuto, asignandole estos valores al objeto Calendar creado por getAgo.

Tal vez, en lugar de regresar delegate.time, podriamos formatear y regresar un String.

La salida final de nuestro codigo sera :

Fri Aug 19 08:26:00 CDT 2011

Comentarios

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.
Imagen de ezamudio

está bueno

Me da algunas ideas de cosas que se le puede agregar para enriquecer un poco el DSL, aunque habría que dejar que at devolviera un Calendar para ser más consistente:

def semanaPasada = 7.days.ago
5.days.after(semanaPasada)
2.days.after(semanaPasada.at(1:15))
3.days.before(semanaPasada)
7.days.fromNow

Sólo habría que implementar tres métodos más:

Integer.metaClass.getAfter = {Calendar d->
  def cal=Calendar.instance
  cal.time=cal.time
  cal.add(Calendar.DAY_OF_MONTH, delegate)
  cal
}

Integer.metaClass.getBefore={Calendar d->
  def cal=Calendar.instance
  cal.time=cal.time
  cal.add(Calendar.DAY_OF_MONTH, -delegate)
  cal
}

Integer.metaClass.getFromNow = {->
  def cal=Calendar.instance
  cal.add(Calendar.DAY_OF_MONTH,delegate)
  cal
}

Imagen de maleficarum

Complementado

Este complemento en Integer es bastante bueno ... Gracias @ezamudio

Imagen de ezamudio

y más allá...

La bronca es que el método "days" te devuelve el mismo Integer, por lo que te lo puedes brincar completamente. Podrías hacer una clase similar al enum TimeUnit y entonces agregar varios métodos a Integer como days, hours, weeks, months, years, que todos devuelvan una instancia de tu clase Tiempo y esa clase sea la que tenga el ago, after, before, fromNow, etc. Así lo enriqueces todavía más:

10.years.ago
2.months.fromNow
4.hours.before(someOtherDate)