Published using Google Docs
DSLs Internos - 27/12/2012 - Pub
Updated automatically every 5 minutes

DSLs Internos - 27/12/2012

Comenzar hablando de DSLs vs GPLs.

- Los GPLs se pueden utilizar para resolver problemas de cualquier dominio. Se dicen que son Turing Complete, lo que significa que pueden realizar cualquier computación (Franco DIXIT)

- Los DSLs, en cambio, son lenguajes diseñados para un dominio en particular. Usualmente no pueden resolver cualquier problema, pero resultan más adecuados para resolver cosas en el dominio para el que fueron diseñados.

Ejemplos de DSLs: SQL, HTML.

¿Cuál es el requisito para un DSL? Un DSL es en realidad cualquier lenguaje que se defina que tenga un propósito específico. En este sentido, un DSL podría ser un subconjunto de un lenguaje de propósito general, y aún así calificar como un DSL.

Un objeto con una interfaz lo suficientemente rica puede definir implícitamente un DSL. Ejemplo, un builder como el de la clase de declaratividad (?).

Pero también pueden existir DSLs que sean más complejos, que no quepan en un subconjunto del lenguaje de propósito general que estamos utilizando, en cuyo caso habrá que crear un parser e intérprete/compilador para ellos.

Con esto podemos hacer una distinción:

- DSLs Internos: Son aquellos que se desarrollan como una interfaz dentro de un lenguaje de propósito general

- DSLs Externos: Son aquellos que necesitan su propio parser e intérprete/compilador, que pueden o no guardar alguna relación con el programa que interfacea con ellos, y que pueden correr incluso en un entorno totalmente distinto.

En esta clase vamos a ver DSLs Internos en Ruby.

------------


Spec

Vamos a crear un tipo particular de test llamado “Spec”. Nuestra intención es que lo que escribamos “especifique” que 5 tiene que ser menor que 10 y mayor que 0, pero que además compile y permita asegurar que nuestra especificación se cumple.

¿Cómo hacemos eso? Test:

@Test

void test5menorQue10yMayorQue0() {

   assertTrue(5 < 10);

   assertTrue(5 > 0);

}

OK, eso más o menos funciona, pero no es muy descriptivo del problema que estamos resolviendo. Usamos mejor JUnit:

void test5menorQue10yMayorQue0() {

   assertTrue(“5 debería ser menor que 10”, 5 < 10);

   assertTrue(“5 debería ser mayor a 0”, 5 > 0);

}

Más legible, pero aún así no se parece a la especificación que hicimos. Lo escrito describe cómo hay que hacer para verificar que se cumple la especificación, pero no la especificación.

Con la sintaxis de Groovy podemos sacar los paréntesis para hacer algo como:

void test5menorQue10yMayorQue0() {

   assertTrue “5 debería ser menor que 10”, 5 < 10

   assertTrue “5 debería ser mayor a 0”, 5 > 0

}

Que está mejor, pero nos falta.

Podemos directamente escribir:

5.deberia ser mayorQue 0 y ser menorQue 10

Pero esto compila? Sí, compila. Corre? No.

Por la sintaxis flexible de Groovy esto equivale a:

5.deberia(ser).mayorQue(0).y(ser).menorQue(10)

(Aclarar que es así cuando especifico el destinatario del mensaje. Cuando no lo especifico, es otra cosa).

Entonces deberíamos hacer que 5 entienda el mensaje deberia, que ser sea un identificador válido y que el resto de los mensajes encadenados tengan sentido.

Empezamos por:

class Spec {

  static {

    Object.metaClass {

      deberia = {

        ...

      }

     

    }

  }

¿Qué parámetros recibe debería? En este caso un verbo que no dice nada. Lo ignoramos. Lo importante es lo que devolvemos, que tiene que ser un objeto que entienda el mensaje mayorQue.

 Object.metaClass {

      deberia = {

        verbo -> new Condicion(valor: delegate)

      }  

    }

class Condicion {

  def valor

  def mayorQue(otroValor) {

    validar("${valor} deberia ser mayor que ${otroValor}", valor > otroValor)

  }

  def validar(mensaje, condicion) {

    Assert.assertTrue(

      mensaje,

      condicion)

    valor

  }  

}

Hacemos que validar devuelva el valor, para poder seguir concatenando mensajes:

 Object.metaClass {

      deberia = {

        verbo -> new Condicion(valor: delegate)

      }

      y = {

        verbo -> delegate.deberia(verbo)

      }  

    }

  def menorQue(otroValor) {

    validar("${valor} deberia ser mayor que ${otroValor}", valor > otroValor)

  }

Y con eso ya debería funcionar.

-----------------

Escenarios

Nos gustaría agregar a nuestro spec la posibilidad de definir escenarios. Cada escenario sería un conjunto de variables con un valor específico. El objetivo sería evaluar que la relación que especificamos se cumpla en cada escenario. Podríamos hacer algo como:

def builder = new RequerimientoBuilder().

builder.esperarQue({ it.deberia ser menorQue 10 }).

builder.siendo({ [4, 5, 6] }).

builder.evaluar()

Cuya implementación sería

class EspecificacionBuilder {

 def valores

 def bloque

 

 def siendo(bloque) {

   this.valores = bloque()

 }

 

 def esperando(bloque) {

   this.bloque = bloque

 }

 def evaluar() {

   valores.each(bloque)

 }

}

Guardamos los valores de cada escenario y el código de la aserción de la relación que se tiene que cumplir para los valores.

Esto funciona, pero podemos mejorar un poco el lenguaje, aprovechando los bloques de Groovy:

Spec.especificacion {

  esperar {

        it.deberia ser mayorQue 0 y menor que 10    

  }

  siendo {

   [4, 5, 6]

  }

}

¿Cómo se traduce esto? Se traduce a:

Spec.especificacion({

esperar({})

siendo({})

})

Donde los destinatarios de esperar y siendo dependen del bloque que le pasamos como parámetro a especificacion

Otros ejemplos con bloques (para dar, si se quiere):

[4, 5, 6].collect({ it + 1})

es equivalen a:

[4, 5, 6].collect(){ it + 1}

[4, 5, 6].collect{ it + 1}

----

[4, 5, 6].inject(0, { x, y -> x + y })

es equivalente a:

[4, 5, 6].inject(0) { x, y -> x + y }

---------

 

Vamos a definir el método especificacion entonces. Queremos que el destinatario de los mensajes del bloque que recibe por parámetro sea un EspecificacionBuilder. (Hablar un poco de como se resuelven los mensajes enviados en bloques en Groovy).

Entonces podemos hacer:

static especificacion(bloque) {

        def builder = new EspecificacionBuilder()

        bloque.delegate = builder

        bloque()

        builder.evaluar()

}

Y esto funciona.

Ahora, como esto de cambiar el delegate antes de ejecutar un bloque es bastante común en Groovy, existe en general un método que lo hace por nosotros que es:

builder.with(bloque)

El with hace lo mismo que hicimos arriba, pero sin efecto de lado: Luego de hacer with el delegate del bloque no permanece cambiado.

----------------

Ahora vamos a meter más variables para la aserción:

especificacion {

          esperando {

            (x+y).deberia ser igual z

          }

          siendo {[

            [x: 4, y: 5, z: 9],

            [x: 1, y: 2, z: 3],

            [x: 1, y: 3, z: 4]

          ]}

        }

}

x, y y z van a ser mensajes que serán enviados al receptor implícito del bloque que se le está pasando como parámetro a esperando.

Groovy nos permite acceder a los mapas de varias formas:

m = [foo: 4, bar: 5]

m.get('foo')

m['foo']

m.foo

Entonces podemos hacer que el receptor implícito del bloque sea directamente el mapa:

class EspecificacionBuilder {

   ...

  def evaluar() {

        valores.each { it.with(bloque)  }

  }

}

FALTA TABLITA