Kotlin DSL | Construcción del DSL: 1 — Organización de paquetes y el objeto ‘Panel’

Glenn Sandoval
Kotlin en Android
Published in
6 min readMay 31, 2021

Organización del proyecto, metodología y construcción del objeto ‘Panel’

En el artículo anterior presenté el código base sobre el cual construiremos un DSL.

🔗 Si aún no has descargado el código base, te recuerdo que lo puedes encontrar en mi cuenta de GitHub en el siguiente enlace: Shapes-DSL

En este artículo iniciaremos con la construcción del DSL y lo haremos siguiendo una metodología parecida o análoga al TDD — guardando las proporciones — .

Como ya mencioné al inicio de esta serie, el objetivo es partir de este código…

Código inicial

… y llegar al siguiente código:

Código final

Iremos cambiando el código poco a poco partiendo del código inicial del método main acercándonos cada vez más al código final.

Estructura de paquetes

Antes de iniciar hay que organizar el código nuevo y para ello crearemos un nuevo paquete en el mismo nivel del paquete console_shapes, bajo la carpeta kotlin y lo nombraremos console_shapes_dsl. Trabajaremos exclusivamente dentro de este paquete y todo el resto del código externo a él quedará intacto, a excepción del método main.

Para crear el DSL nos apoyaremos mucho en el patrón de diseño ‘Constructor’ y en las funciones de extensión, por esta razón dentro del nuevo paquete console_shapes_dsl crearemos otros 2 paquetes a los que nombraremos builders y external.

Dentro del paquete builders agregaremos las clases correspondientes a los constructores intermediarios de los objetos sobre los que necesitemos aplicar el patrón de diseño ‘Constructor’.

Dentro del paquete external agregaremos un archivo que tendrá todas las funciones de extensión, funciones de orden superior, funciones de ayuda o cualquier código que claramente no pertenece a una estructura definida o que no sigue una jerarquía.

💬 Aunque muchas de las funciones de extensión las podríamos definir como funciones miembro dentro de las clases “constructoras” que vamos a crear, lo haremos de esta manera para ilustrar el caso en el que tampoco se nos posibilita la modificación del código dentro de los constructores ya que puede ser posible que alguna vez queramos crear un DSL a partir de código que internamente ya implementa el patrón de diseño ‘Constructor’.

En este punto deberías tener una lista de directorios como la que se muestra en la siguiente imagen:

Shapes-DSL: Estructura de directorios

Construcción del Panel

Lo primero que haremos será modificar mínimamente el método main cambiando las instrucciones

por

💬 Los puntos suspensivos corresponden a todo el código que se encuentra entre dichas instrucciones y que por el momento no modificaremos hasta hacer funcionar el nuevo cambio.

Este cambio provoca que el código no compile, pero no te preocupes porque así tiene que ser. La idea es hacer que funcione y luego seguir avanzando poco a poco de la misma forma hasta llegar al código final.

El código en este punto se ve de la siguiente manera:

main.kt — Paso 1

Observa detenidamente la instrucción panel { ... }. Podemos inferir que es una función de orden superior que retorna un objeto de tipo Panel. Además dado el bloque de código sabemos que su último (y único) parámetro es un lambda — trailing lambda — .

Solo nos queda inferir el tipo de función del lambda. Para que el código que está dentro de las llaves funcione debe haber un parámetro llamado panel de tipo Panel, lo cual implica que el lambda lo recibe como parámetro de entrada. Además, como la última línea del lambda es una función que no retorna un valor podemos concluir que el valor de retorno del lambda es de tipo Unit. El tipo de función del lambda correspondería entonces a (Panel) -> Unit. El signature de la función de panel sería el siguiente:

fun panel(init: (Panel) -> Unit): Panel

💬 Llamo init al lambda dado que en esencia su bloque de código contiene la creación de figuras y su inserción en el Panel.

Ahora que hemos inferido el signature de la función panel llegó el momento de escribir su código. Crea un archivo Kotlin llamado Extension.kt dentro del paquete console_shapes_dsl.external y escribe lo que hasta este momento tenemos:

fun panel(init: (Panel) -> Unit): Panel {

}

Como valor de retorno sabemos que es un objeto de tipo Panel. Además hay que invocar el código del lambda que casualmente recibe un objeto de tipo Panel, esto nos da una pista de que tanto el objeto de retorno como el que usaremos al invocar al lambda es el mismo. El código momentáneamente quedaría así:

fun panel(init: (Panel) -> Unit): Panel {
val panel = Panel()
init(panel)
return panel
}

Sin embargo, podemos valernos de la función de ámbito apply para reducir el cuerpo de la función a una sola línea:

fun panel(init: (Panel) -> Unit): Panel {
return Panel().apply { init(this) }
}

Localiza el método main e incluye las importaciones necesarias en el archivo:

import console_shapes_dsl.external.panel

Verás que te marca las referencias panel como erróneas dado que no existe una variable llamada panel y además las confunde con llamadas a la función que acabamos de crear. Para solucionar esto basta con añadir la notación de flecha al lambda y nombrar la variable como panel:

panel { panel ->
...
}

💬 Si ejecutas el método main en este punto, verás que nuevamente funciona.

Estarás de acuerdo conmigo en que nombrar a la variable igual que a la función se presta para confusiones, así que podríamos cambiarle el nombre a la variable panel o eliminar la notación de flecha y usar la referencia it en su lugar:

panel {
...
it.addShape(square)
...
}

Volverás a estar de acuerdo conmigo en que ahora usando la referencia it queda menos claro. Sería mejor si pudiéramos usar la referencia this dado que estando dentro de un bloque de código de una función llamada panel es más intuitivo asumir que todo se hace dentro del contexto de un objeto de tipo Panel.

Para lograr lo anterior debemos cambiar un poco el tipo de función del lambda. Ya no deberá recibir parámetros de entrada sino que ahora será un lambda con receptor y dicho receptor será de tipo Panel, es decir: Panel.() -> Unit. El signature de la función panel quedaría así:

fun panel(init: Panel.() -> Unit): Panel

Ahora el lambda podrá ser invocado solamente por un objeto de tipo Panel y dicha invocación ya no requerirá parámetros de entrada:

fun panel(init: Panel.() -> Unit): Panel {
val panel = Panel()
panel.init()
return panel
}

Si nos volvemos a valer de la función de ámbito apply podemos reducir el cuerpo de la función a una sola línea. La función panel quedaría finalmente de la siguiente manera:

Ahora desplázate nuevamente al método main y cambia todas las referencias a la variable panel o it dentro del bloque de código de la función panel y reemplázalas por referencias this:

panel {
...
this.addShape(square)
...
}

O incluso mejor, puedes remover las referencias this dejando solamente las instrucciones, lo que resulta en una mayor fluidez al leerlo. El código del método main quedaría momentáneamente de la siguiente manera:

main.kt — Paso 2

💬 Si ejecutas el código verás que nuevamente funciona.

Finalizaré este artículo en este punto ya que es importante que lo tengas todo claro antes de que sigas avanzando. Si aún no te queda alguna parte clara te sugiero volver a repetir los pasos de este artículo con detenimiento. Lo que falta es muy similar así que se te hará mucho más sencillo habiendo comprendido los pasos de este artículo.

Continúa en el siguiente artículo con Construcción del DSL: 2 — El objeto ‘Cuadrado’

--

--

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.