DSL en Groovy : Builders
Groovy ofrece varias opciones para crear tu propio DSL, siendo una de ellas, y el tema de este artículo, los Builders ("Constructores").
Los builders permiten construir estructuras jerárquicas de manera natural, eliminando la complejidad de que el desarrollador administre la creación de la estructura al mismo tiempo que el código de negocio.
Groovy ofrece 3 opciones para que puedas crear tus propios builders:
- extendiendo GroovyObjectSupport, es tu responsabilidad alambrar la lógica al sobreescribir invokeMethod/methodMissing principalmente
- extendiendo BuilderSupport, la manera estandar de muchos builders que existen en la distribución de Groovy
- extendiendo FactoryBuilderSupport, la manera mas moderna, aumenta los mecanismos provistos por BuilderSupport
Supongamos que necesitamos una clase que obtenga un SQL desde un archivo properties y que nos regrese una cadena con dicho SQL
Primero, debemos definir nuestro builder:
//Usamos ConfigSluprper, que nos ayudara cargar las propiedades desde el archivo indicado
def config = new ConfigSlurper().parse(new File('statements.properties').toURL()).toProperties()
//Este metodo asignara como delegado en la ejecucion a esta clase. Si no se hace esto, entonces el alcance o ambito de ejecucion
//no sera esta clase, si no el script del closure;
def build(acciones) {
acciones.delegate = this
acciones()
}
//Dado que no hemos definido los metodos 'select' o 'delete' en nuestro builder, en tiempo de ejecucion se llamara a este metodo
//en el cual, para no implementar todas las varianes que podemos tener en metodos separados, incluimos aqui nuestra logica
def methodMissing(String accion,args){
def statement
//Si se envia un argumento en el metodo, buscamos el statement por nombre ('accion.valor' o 'select.usuario')
if(args) {
statement = config."${accion}.${args[0]}"
} else {
statement = "SELECT 1"
}
//Regresamos el estatement
statement
}
}
Finalmente, lo usamos desde cualquier clase o script groovy:
//Este closure es el que se ejecuta dentro del metodo build de nuestro DBBuilder
def s = select("usuario")
def d = delete("usuario")
//Ejecutar todo el SQL resultante
};
Ahora, hagamos unos cambios para permitir la ejecucion de los statements en el mismo builder (esto solo es para ejemplificar a los Builders y no pretende ser una propuesta de solucion en la vida real):
class DBBuilder {
//Usamos ConfigSluprper, que nos ayudara cargar las propiedades desde el archivo indicado
def config = new ConfigSlurper().parse(new File('statements.properties').toURL()).toProperties()
def sql
//Sabemos que debemos recibir en el constructor un mapa con los datos de conexion
def DBBuilder(configConexion) {
sql = Sql.newInstance(configConexion.url, configConexion.user, configConexion.password, configConexion.driver)
}
//Este metodo asignara como delegado en la ejecucion a esta clase. Si no se hace esto, entonces el alcance o ambito de ejecucion
//no sera esta clase, si no el script del closure;
def build(acciones) {
acciones.delegate = this
acciones()
}
//Dado que no hemos definido los metodos 'select' o 'delete' en nuestro builder, en tiempo de ejecucion se llamara a este metodo
//en el cual, para no implementar todas las varianes que podemos tener en metodos separados, incluimos aqui nuestra logica
def methodMissing(String accion,args){
def statement
def params = []
//Si se envia un argumento en el metodo, buscamos el statement por nombre ('accion.valor' o 'select.usuario')
if(args) {
statement = config."${accion}.${args[0]}"
} else {
statement = "SELECT 1"
}
if(args.length > 1) {
params = ((List) args).remove(1)
}
sql.rows(statement, params)
}
}
Finalmente, nuestra llamada al builder sera asi :
def s = select("usuario",[1])
def d = delete("usuario",[1])
def ss = select("usuarios",[])// o select("usuarios")
println s
println d
println ss
};
y nuestro archivo statements es un properties de la siguiente forma :
select.usuarios="SELECT * FROM USUARIO"
delete.usuario="DELETE FROM USUARIO WHERE ID = ?"
- maleficarum's blog
- Inicie sesión o regístrese para enviar comentarios
Comentarios
Dices que hay tres opciones
Dices que hay tres opciones para crear un builder:
Pero en tu código no usas ninguno de los tres.
Luego mencionas que es un DSL ( Lenguaje de Dominio Específico ) pero el código que se muestra es Groovy mismo. No parece ser un lenguaje diferente.
Eso sí, esta muy interesante las cosas que mencionas, como el "methodMissing", que seguramente fue tomado de Ruby ( el lenguaje que el creador de Groovy quería emular ) que a su vez lo tomó de Smalltalk y se ejecuta cuando se llama a un método que la clase actual no contempla.
es la primera
Oscar, extender GroovyObjectSupport no es poner "extends bla" en la definicion de la clase... como dice el post, hay que sobreescribir invokeMethod y/o methodMissing, y es lo que hace el DBBuilder.
Los DSL's en Groovy no dejan de ser llamadas a Groovy, lo que se hace es que se puede construir una sintaxis dinamica para interpretar un buen de cosas, aprovechando cosas de Groovy como setters y getters tipo propiedades (asignacion, etc), o que te puedes saltar los parentesis en algunas llamadas y que el salto de linea es un terminador de sentencia, y cosas como el "with" de Groovy. Esto es un ejemplo simple, pero imaginate si consideras un tercer argumento que fuera un closure que le pasas al each del resultado de sql.rows, entonces podrias hacer algo asi:
select("usuario") {
println "${row.id} ${row.nombre}"
}
}
En Gradle por ejemplo cuando haces algo como esto:
defaultTasks 'build'
dependsOnChildren()
apply plugin:'java'
todo eso termina siendo codigo que se invoca sobre un objeto tipo Project, es como si hicieras:
p.setVersion('1.0)
p.setDefaultTasks('build')
p.dependsOnChildren()
p.apply([plugin:'java'])
No puse detalles
Oscar, tienes razon y no aclare: este es el primer ejemplo de varios que pondre sobre DSL, que incluye algo mas de builders, uso de metaclass, etc. Pero como dice Enrique, parece ser groovy todo, por que que de hecho es groovy. Al final, la intencion es ejemplificar como haces para que los desarrolladores usen "tus modulos, plugins o api's" de una manera mas "coherente"; en este ejemplo, buscas los statements como una llamada a algun metodo (que no existe), pero que tu "api" sabe que debe buscar como prefijo y no como funcion. Finalmente, para el usuario es mas "intuitivo" ejecutar :
select("usuario");
}
que
sql = //abrir conexion a BD
sql.eachRow("SELECT * FROM usuario",[1]) { it }
y como comenta tambien Enrique, podriamos modificarlo para pasarle un closure que haga algo con los resultados; entonces podrias ejecutar todo lo que tu quieras despues de obtener los resultados, que al final, lo importante y la razon de cada API es que no tengas que estar lidiando con lo de siempre, y solo enfocarte a la logica del negocio.
Gracias por aclarar lo 3 puntos (que especificare en el proximo post).
Si, ya veo, ahora tiene
Si, ya veo, ahora tiene sentido.
:)
¿Un DSL para el dominio de la solución?
Creo que Oscar tiene razón cuando dice que no ve un DSL en los ejemplos.
Un DSL trata de buscar en la sintaxis del programa un vocabulario común con los usuarios del dominio y los implementadores de la solución, de ahí el término "lenguaje específico del dominio". En los ejemplos , tanto los usuarios como los implementadores conocen el vocabulario del lenguaje de programación y por supuesto que no hay mejor DSL para el dominio de la solución que el propio lenguaje de programación, aprovechando cosas como las que en este post se están comentando.
No basta tener un vocabulario común para que un DSL tenga éxito; es necesario que además de tener una correspondencia directa de los nombres del dominio en el espacio de la solución, también se pueda usar el mismo lenguaje para describir las interacciones y colaboraciones en el dominio del problema. Groovy, Ruby y Scala son buenos para eso.