Kotlin DSL | Construcción del DSL: 2 — El objeto ‘Cuadrado’
Construcción del objeto ‘Cuadrado’
Puedes ir a cualquier artículo de esta serie haciendo clic en alguno de los siguientes enlaces:
Kotlin DSL
- Introducción
- Conocimiento base para construir DSLs con Kotlin — Parte 1
- Conocimiento base para construir DSLs con Kotlin — Parte 2
- Código Base: Proyecto Shapes-DSL
- Construcción del DSL: 1 — Organización de paquetes y el objeto ‘Panel’
- Construcción del DSL: 2 — El objeto ‘Cuadrado’
- Construcción del DSL: 3 — Los objetos ‘Triángulo’ y ‘Rombo’
- Construcción del DSL: 4 — Espacios en blanco y el objeto ‘Figura Compuesta’
- Construcción del DSL: 5 — Los operadores ‘plus’ y ‘minus’ y funciones ‘inline’
- Construcción del DSL: 6 — La anotación @DslMarker
- Experimentación y conclusiones
En el artículo anterior definimos la metodología a seguir y la estructura de paquetes para organizar el código. También quedó establecida la función panel
. Al final del artículo anterior el contenido del método main
lucía de la siguiente manera:
En este artículo continuaremos con los cambios en el bloque de código de la función panel
de tal manera que al finalizar luzca así:
Construcción de la figura geométrica ‘Cuadrado’
Nuestro próximo objetivo es reemplazar la instanciación de la figura geométrica de tipo Square
dentro del bloque de código de la función panel
, así como su posterior inserción dentro del Panel. Es decir, reemplazaremos las instrucciones…
…por…
Podemos observar que la instrucción square { ... }
es una función de orden superior que retorna un objeto de tipo Square
. Además ya no vemos la instrucción addShape
con lo cual podemos asumir que la función no solamente crea el objeto de tipo Square
, sino que también lo incluye dentro del Panel y luego lo retorna.
Para incluir el objeto dentro del Panel se necesita una referencia hacia dicho Panel, sin embargo, no observamos que se esté pasando como parámetro. Es posible invocar a la función addShape
debido a que ésta es una función miembro de la clase Panel
. Esto nos da una pista de lo que debemos hacer para que la función square
pueda incluir el objeto de tipo Square
dentro del Panel sin que sea necesario pasárselo como parámetro. Si hacemos que la función square
sea una función de extensión de la clase Panel
bastaría con invocarla por medio de un objeto de tipo Panel
. El signature de la función square
sería el siguiente por el momento:
fun Panel.square(?): Square
Observa que dentro de los paréntesis como parámetro de entrada pongo un signo de interrogación ?
debido a que en este momento lo único que tenemos claro es que recibe un lambda. Vamos a proceder ahora a inferir dicho lambda:
{
lines = 8
char = 'd'
}
Dentro del bloque de código del lambda podemos notar claramente que las dos instrucciones lines = 8
y char = 'd'
corresponden a los parámetros que recibe un objeto de tipo Square
durante su construcción. Dado que las propiedades lines
y char
del objeto de tipo Square
son inmutables y solo se pueden establecer al instanciarlo, podemos inferir que hay un objeto intermediario que construye el objeto de tipo Square
y que nos permite construir el objeto paso a paso. Este objeto intermediario se ajusta al patrón de diseño ‘Constructor’.
Antes de continuar con el análisis del lambda debemos crear un constructor de objetos de tipo Square
. Para ello desplázate hasta el paquete console_shapes_dsl.builders
y crea una nueva clase Kotlin llamada SquareBuilder
. La implementación quedaría de la siguiente manera:
Siguiendo el patrón de diseño ‘Constructor’ es necesario que se declaren las mismas propiedades que requiere el objeto de tipo Square
y que se les establezca un valor por defecto. En este caso el número de líneas por defecto será 0 y el caracter por defecto será un asterisco. La función build
instanciará el objeto de tipo Square
y lo retornará.
💬 Los nombres de las propiedades podrían ser diferentes si se desea, sin embargo, es más claro si se mantienen en correspondencia con las propiedades de la clase que se pretende instanciar.
Volviendo al análisis del lambda y habiendo creado una clase que nos servirá de intermediaria para la creación de objetos de tipo Square
podemos inferir que su bloque de código se ejecuta dentro del contexto de un objeto de tipo SquareBuilder
. Si observas bien, la instrucción lines = 8
se podría expresar como this.lines = 8
y la referencia this
correspondería justamente a un objeto de tipo SquareBuilder
.
💬 De haber declarado las propiedades con otro nombre, tendríamos que modificar las instrucciones
lines = 8
ychar = '*'
por los nombres que les hayamos puesto a dichas propiedades.
Ahora que tenemos claro que el contexto dentro del bloque de código del lambda es un objeto de tipo SquareBuilder
, inferimos que es un lambda con receptor y que dicho receptor es de tipo SquareBuilder
. También vemos claramente que el lambda no recibe parámetros de entrada dado que no muestra la declaración de variables con la notación de flecha y tampoco usa la referencia it
en su bloque de código. La última línea del bloque de código del lambda no retorna un valor así que inferimos que el lambda no retorna un valor, o lo que es lo mismo, retorna un valor de tipo Unit
. Luego de hacer este análisis concluimos que el tipo de función del lambda corresponde a SquareBuilder.() -> Unit
.
Ahora que tenemos definido el tipo de función del lambda, retomamos el análisis de la función square
desde donde habíamos quedado, es decir, la instrucción square
es una función de extensión de la clase Panel
y a su vez es también una función de orden superior que recibe el lambda y que retorna un objeto de tipo Square
, quedando el signature de la siguiente manera:
fun Panel.square(init: SquareBuilder.() -> Unit): Square
💬 Llamo
init
al lambda dado que en esencia su bloque de código contiene el establecimiento de las variables que se utilizarán para la construcción del objeto de tipoSquare
.
Ahora que tenemos definido el signature de la función square
, desplázate hasta el paquete console_shapes_dsl.external
y añade el siguiente código en el archivo Extension.kt
:
fun Panel.square(init: SquareBuilder.() -> Unit): Square {
}
Sabemos que la función debe cumplir con los siguientes puntos:
— Crear un objeto de tipo Square
con los parámetros correspondientes al bloque de código del lambda.
— Incluir el objeto de tipo Square
dentro del Panel.
— Retornar el objeto de tipo Square
.
La implementación de la función quedaría de la siguiente manera:
fun Panel.square(init: SquareBuilder.() -> Unit): Square {
val builder = SquareBuilder()
builder.init()
val square = builder.build()
this.addShape(square)
return square
}
Finalmente podríamos reducir el código de la función square
a una sola línea valiéndonos de las funciones de ámbito apply
y also
de la siguiente manera:
Si te desplazas al método main
te darás cuenta de que ya no te marca errores. Por el momento así debería lucir el método main
:
💬 Si ejecutas el código verás que nuevamente funciona.
Por ahora lo que hemos hecho es suficiente. Las figuras ‘Triángulo’ y ‘Rombo’ se harán prácticamente igual por lo que será importante abstraer el comportamiento en una clase superior de la cual se pueda heredar y así evitar la redundancia, sin embargo, lo haremos al finalizar las 3 figuras para que queden más claras en su estructura y de paso consolidar el conocimiento por repetición. Puedes incluso intentarlo por tu cuenta y comparar el resultado con lo que haremos en el siguiente artículo.
Continúa en el siguiente artículo con Construcción del DSL: 3 — Los objetos ‘Triángulo’ y ‘Rombo’
- Puedes acceder a la documentación oficial sobre funciones de orden superior y lambdas en el siguiente enlace: https://kotlinlang.org/docs/lambdas.html
- Puedes acceder a la documentación oficial sobre lambdas con receptor en el siguiente enlace: https://kotlinlang.org/docs/lambdas.html#function-literals-with-receiver
- Puedes acceder a la documentación oficial sobre funciones de ámbito en el siguiente enlace: https://kotlinlang.org/docs/scope-functions.html