En el artículo anterior finalizamos con un DSL funcional pero incompleto ya que aún me falta mostrarte un detalle muy importante que debes tener presente. En este artículo mejoraremos el DSL para que tenga ciertas restricciones de acceso dentro de los ámbitos de los lambdas de las funciones de orden superior, de manera que el propio compilador nos alerte de posibles fallos o usos incorrectos.

Control dentro de los ámbitos

Aunque el código que hemos escrito es totalmente funcional y parece finalizado, el hecho de emplear lambas y funciones de orden superior con extensión permite muchísima flexibilidad al construir los diferentes objetos. Esta característica puede sonar como algo muy positivo, sin embargo, en algunos casos, disponer de tanta flexibilidad permite hacer un uso incorrecto o indebido del DSL. Para “solucionar” este problema se debe ejercer un control dentro de los ámbitos.

💬 Pongo “solucionar” entre comillas porque aunque se mitiga el problema, hay maneras de saltarse dicho control.

Con el control dentro de los ámbitos me refiero a una correcta construcción de objetos dentro de los bloques de código de cada lamba. Por ejemplo, con el estado actual del DSL se pueden hacer cosas como ésta:

Estarás de acuerdo conmigo en que dentro de un lambda que construye una figura no se debería poder crear otra figura, con excepción del bloque de código dentro de una figura compuesta. Este problema radica en el ámbito dentro de cada lambda. Cuando se tiene un lambda con receptor, anidado dentro de otro lambda con receptor, dentro del lambda interno se pueden invocar a las propiedades y funciones miembros del receptor del lambda que lo contiene.

Koltin nos ofrece la anotación @DslMarker para crear anotaciones que agrupen receptores y limitar su acceso. Esta anotación es aplicable a clases que definen anotaciones.

La anotación @DslMarker:

Las clases que definen anotaciones marcadas con la anotación @DslMarker se usan para definir DSLs. La anotación creada se usa para marcar clases o receptores impidiendo así que los receptores implícitos marcados con la misma anotación puedan ser accedidos dentro del mismo ámbito.

Por ejemplo, en nuestro caso queremos que dentro del lambda correspondiente a la función panel se puedan invocar sus funciones de extensión square, triangle y rhombus, pero que dentro de los lambdas de dichas funciones no se puedan invocar las funciones de extensión de la clase Panel ni cualquiera de sus propiedades o funciones miembro en general, es decir, que no se tenga acceso al receptor implícito de tipo Panel. Para lograr esto es necesario marcar los lambdas con la misma anotación.

Desplázate hasta el paquete console_shapes_dsl.external y modifica el archivo Extension.kt y crea una nueva anotación que defina un DSL de la siguiente manera:

Ahora que tenemos definido un DSL, podemos agrupar los lambdas cuyo acceso a sus receptores queremos limitar. Empecemos con el lambda de la función panel y el lambda de la función square. Nuevamente desplázate al paquete console_shapes_dsl.external y modifica el archivo Extension.kt. Ubica la función panel y la función de extensión square y marca los receptores de sus respectivos lambdas con la anotación @ShapeDsl de la siguiente manera:

💬 Desplázate hasta el método main e intenta crear una figura de tipo ‘Triángulo’ dentro del bloque de código de la figura de tipo ‘Cuadrado’. Verás que ahora te marca un error de compilación, sin embargo, si lo haces al revés, crear una figura de tipo ‘Cuadrado’ dentro del bloque de código de la figura de tipo ‘Triángulo’, te dejará hacerlo sin problemas. Esto pasa porque aún no hemos marcado el lambda de la función triangle y lo mismo pasaría con la función rhombus.

Ésta es una manera de restringir el acceso a los receptores implícitos. Sin embargo, la manera más apropiada de hacerlo es marcar la clase con la anotación @ShapeDsl en lugar de marcar solamente el receptor del lambda.

💬 En el caso de la función panel no tenemos otra opción que hacerlo así ya que desde el inicio de esta serie de artículos asumimos que el código inicial no iba a ser modificado, simulando ser código del que no tenemos acceso. En caso contrario le pondríamos la anotación a la clase Panel. Las clases constructoras de figuras sí fueron implementadas por nosotros, por lo tanto, a estas clases sí las vamos a marcar con la anotación @ShapeDsl.

Elimina la anotación @ShapeDsl del receptor del lambda de la función square dejándola como estaba antes:

Desplázate hasta el paquete console_shapes_dsl.builders y modifica la clase SquareBuilder marcándola con la anotación @ShapeDsl:

💬 Nuevamente verás que no es posible crear una figura de tipo ‘Triángulo’ dentro del bloque de código de la figura de tipo ‘Cuadrado’, pero sí al revés.

En este punto deberíamos marcar también las clases TriangleBuilder y RhombusBuilder con la anotación @ShapeDsl, sin embargo, dado que las tres clases que requieren anotación extienden de la clase ShapeBuilder , es mejor marcar solamente la clase ShapeBuilder. Esto repercutirá en todas sus subclases y en caso de que se agreguen más figuras, heredarían directamente esta característica.

Desplázate hasta el paquete console_shapes_dsl.builders y elimina la anotación @ShapeDsl de la clase SquareBuilder:

Ahora modifica la clase ShapeBuilder y márcala con la anotación @ShapeDsl:

💬 Desplázate hasta el método main e intenta crear una figura de tipo ‘Triángulo’ dentro del bloque de código de la figura de tipo ‘Cuadrado’ y viceversa. Verás que ahora te marca un error de compilación en ambos sentidos. Incluso puedes probar con las figuras de tipo ‘Rombo’ y tampoco te lo permitirá.

Si te preguntas si deberíamos restringir también la creación de figuras dentro del bloque de código de la función composed, la respuesta es: No, porque una figura compuesta se construye a partir de otras figuras y éstas deberían poder ser construidas directamente dentro de su bloque de código.

Solo me queda advertirte que esta restricción se puede saltar haciendo referencia explícita hacia el receptor del lambda restringido. Por ejemplo, si quisiéramos poder construir una figura de tipo ‘Triángulo’ dentro del bloque de código de una figura de tipo ‘Cuadrado’ lo tendríamos que hacer así:

Observa la referencia this@panel. Hacer referencia hacia el receptor del lambda de manera explícita permite saltarse la restricción de acceso ya que ésta es solamente aplicada a los receptores implícitos.

Solo para que haya claridad, el archivo Extension.kt dentro del paquete console_shapes_dsl.external luce de la siguiente manera:

Además, solamente la clase ShapeBuilder debe estar marcada con la anotación @ShapeDsl.

Finalmente cumplimos con nuestro objetivo inicial. Logramos substituir el código…

main.kt — Inicial

…por el código…

main.kt — Final

…construyendo un DSL, sin haber modificado ni un solo caracter del código base, obteniendo exactamente la misma salida:

Salida en consola

🔗 Puedes acceder al proyecto completo desde mi cuenta de GitHub en el siguiente enlace: https://github.com/pencelab/Shapes-DSL. En la rama master encontrarás solamente el código base. En la rama dsl encontrarás el código base junto al código que construimos a lo largo de esta serie de artículos.

Esto es todo en lo referente a la creación de un DSL. Si llegaste hasta aquí puedes darte una palmadita en la espalda ya que a pesar de que este tema no es difícil, se hace un poco complejo de entender a primera vista, sobre todo si no se tiene un dominio principalmente sobre las funciones de orden superior, los lambdas y las funciones de extensión, o en términos generales, sobre lo básico acerca del paradigma de la programación funcional.

Solo nos queda jugar y experimentar un poco con el DSL y observar algunas de las cosas que se pueden hacer con él. Esto lo haremos en el siguiente artículo y último en el que también sacaremos algunas conclusiones.

Continúa en el siguiente artículo con Experimentación y conclusiones

--

--

Glenn Sandoval
Kotlin en Android

I’m a software developer who loves learning and making new things all the time. I especially like mobile technology.