Kotlin DSL | Construcción del DSL: 4 — Espacios en blanco y el objeto ‘Figura Compuesta’
Inserción de espacios en blanco entre figuras y combinación de figuras para crear figuras compuestas
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 quedaron definidas las clases constructoras de objetos de tipo Triangle
y Rhombus
y la funciones de extensión triangle
y rhombus
que utilizan dichas clases constructoras para crear y añadir ‘Triángulos’ y ‘Rombos’ dentro del Panel. También se definió la clase abstracta ShapeBuilder
que reúne las características comunes entre las clases constructoras. Al final del artículo anterior el contenido del método main
lucía de la siguiente manera:
En este artículo cambiaremos la manera en que añadimos espacios en blanco y también la manera en que creamos y añadimos figuras compuestas dentro del Panel.
La función ‘space’ para añadir espacios en blanco
El próximo objetivo es cambiar todas las instrucciones addShape(Space)
por instrucciones space()
quedando el código del método main
así:
Claramente la instrucción space()
es una invocación a una función. Podemos inferir entonces que dentro de dicha función se ejecuta la instrucción addShape(Space)
para añadir el objeto Space
dentro del Panel.
💬 Recuerda que la instrucción
addShape(Space)
lleva implícita la referenciathis
siendo también posible invocarla así:this.addShape(Space)
.
La función space
no es tan diferente a las funciones square
, triangle
y rhombus
siendo que el objeto Space
también es una figura pues extiende a la clase Shape
. Podemos afirmar entonces que para invocar a la función addShape
dentro de la función space
necesitamos una referencia hacia el objeto de tipo Panel
tal y como también la necesitamos dentro de las otras tres funciones. Podemos concluir que la función space
es una función de extensión de la clase Panel
, al igual que las funciones square
, triangle
y rhombus
.
El signature de la función space
quedaría así:
fun Panel.space()
Desplázate hasta el paquete console_shapes_dsl.external
y añade el siguiente código en el archivo Extension.kt
:
fun Panel.space() {
}
El único objetivo de la función es añadir la figura de tipo Space
dentro del Panel. La implementación de la función es la siguiente:
💬 Si ejecutas el código verás que nuevamente funciona.
Construcción de la figura geométrica compuesta
El próximo objetivo es reemplazar las instrucciones addShape(ComposedShape(shapeX, shapeY, ComposedShape.Operation.XYZ))
por instrucciones composed { shapeX xyz shapeY }
, donde shapeX
y shapeY
pueden ser instancias de las clases Square
, Triangle
, Rhombus
o ComposedShape
y Operation.XYZ
o xyz
corresponden a la operación a realizar, ya sea Operation.UNION
u Operation.INTERSECTION
.
Podemos separar dichas instrucciones en 2 partes:
- Cambiar la instrucción
ComposedShape(shapeX, shapeY, ComposedShape.Operation.XYZ)
por la instrucciónshapeX xyz shapeY
- Cambiar la invocación a la función
addShape
por la invocación a la funcióncomposed
Además ambas partes deben ser intercambiables, es decir, al tener expresiones como composed { ComposedShape(shapeX, shapeY, ComposedShape.Operation.XYZ) }
o addShape(shapeX xyz shapeY)
el resultado debería ser el mismo.
Dadas estas equivalencias podemos decir que la instrucción shapeX xyz shapeY
crea un objeto de tipo ComposedShape
y que la invocación a la función composed
agrega una figura de tipo ComposedShape
dentro del Panel.
🔗 Si se te hace un poco extraña la instrucción
shapeX xyz shapeY
puedes echarle un vistazo a al apartado Notación infix que expuse en un artículo anterior.
— La funciones union
e intersection
en notación infix
Vamos a iniciar substituyendo la instrucción ComposedShape(shapeX, shapeY, ComposedShape.Operation.XYZ)
por la instrucción shapeX xyz shapeY
. El código del método main
al que queremos llegar es el siguiente:
Las nuevas funciones y sus respectivas tareas son las siguientes:
- La función
union
debe realizar la unión de dos figuras de tipoShape
y retornar una nueva figura de tipoComposedShape
. La operación a realizar corresponde al parámetrooperation
cuyo valor esOperation.UNION
. - La función
intersection
debe realizar la intersección de dos figuras de tipoShape
y retornar una nueva figura de tipoComposedShape
. La operación a realizar corresponde al parámetrooperation
cuyo valor esOperation.INTERSECTION
.
Los signatures de las funciones union
e intersection
son los siguientes:
infix fun Shape.union(shape: Shape): ComposedShape
infix fun Shape.intersection(shape: Shape): ComposedShape
Según el código al que queremos llegar, las funciones union
e intersection
se deben poder invocar con notación infix y dado que una función infix tiene como restricción recibir solamente 1 parámetro de entrada, será necesario que sean funciones de extensión de la clase Shape
de manera que podremos usar la referencia this
y el parámetro de entrada de tipo Shape
para hacer la combinación de ambas figuras.
Desplázate hasta el paquete console_shapes_dsl.external
y añade el siguiente código en el archivo Extension.kt
:
infix fun Shape.union(shape: Shape): ComposedShape {
}
infix fun Shape.intersection(shape: Shape): ComposedShape {
}
La función union
deberá crear una figura de tipo ComposedShape
realizando la combinación de ambas figuras con la operación Operation.UNION
y luego retornar dicha figura. La función de extensión union
quedaría de la siguiente manera:
De manera similar, la función intersection
deberá realizar la combinación con la operación Operation.INTERSECTION
. La función de extensión intersection
quedaría de la siguiente manera:
💬 Si ejecutas el código verás que nuevamente funciona.
— La función composed
para añadir un objeto ComposedShape
al Panel
Ahora que ya contamos con la creación de figuras de tipo ComposedShape
utilizando la notación infix, debemos cambiar las invocaciones a la función addShape
por invocaciones a la función composed
que lo único que hará será añadir un objeto de tipo ComposedShape
al Panel. El código del método main
al que queremos llegar es el siguiente:
Vamos a analizar la función composed
por partes:
—Los bloques de código entre llaves implican que la función composed
recibe como único parámetro de entrada un lambda. Es decir, la función composed
es una función de orden superior.
—Todos los bloques de código correspondientes al lambda finalizan con invocaciones a las funciones union
e intersection
, por lo tanto, el lambda debe retornar un objeto de tipo ComposedShape
.
— Ninguno de los bloques de código correspondientes al lambda presenta la notación con la flecha, y siendo que no se observa el uso de ninguna referencia ni tampoco de la referencia implícita it
, se asume que el lambda no recibe parámetros de entrada.
— Dentro de los lambda hay invocaciones a las funciones union
e intersection
que son funciones de extensión de la clase Panel
, por lo tanto, es claro que el bloque de código del lambda se ejecuta dentro del ámbito de un objeto de tipo Panel
, tratándose entonces de un lambda con receptor de tipo Panel
.
Algo que no podemos inferir directamente del código al que queremos llegar es el tipo de dato de retorno de la función composed
. Aparentemente no debería retornar nada dado que no se hace una asignación a alguna variable en ninguna de sus invocaciones. Sin embargo, dado que la función composed
está al mismo nivel que las funciones square
, triangle
y rhombus
y en esencia realiza la misma tarea que dichas funciones — crear un figura e ingresarla dentro del Panel — podríamos dotarla de un valor de retorno que corresponda al objeto de tipo ComposedShape
que añade al Panel. De esta manera si se deseara mantener una referencia hacia dicho objeto, eventualmente se podría usar para ser combinado con otras figuras.
Luego de este análisis, el signature de la función composed
quedaría de la siguiente manera:
fun Panel.composed(init: Panel.() -> ComposedShape): ComposedShape
💬 Llamo
init
al lambda para mantener el estilo utilizado en las otras funciones homólogas a ésta.
Desplázate hasta el paquete console_shapes_dsl.external
y añade el siguiente código en el archivo Extension.kt
:
fun Panel.composed(init: Panel.() -> ComposedShape): ComposedShape {
}
Antes de escribir el cuerpo de la función composed
puntualicemos lo que debe hacer:
— Ejecutar el bloque de código del lambda y guardar en una variable el objeto de tipo ComposedShape
que retorna.
— Agregar el objeto de tipo ComposedShape
dentro del Panel.
— Retornar el objeto de tipo ComposedShape
.
La definición de la función composed
quedaría de la siguiente manera:
fun Panel.composed(init: Panel.() -> ComposedShape): ComposedShape {
val shape = init()
this.addShape(shape)
return shape
}
La función anterior la podemos reducir a una sola línea valiéndonos de la función de ámbito also
de la siguiente manera:
💬 Si ejecutas el código verás que nuevamente funciona.
Puedes desplazarte hasta el método main
y probar que tanto la función composed
como la invocación a las funciones infix son intercambiables con la invocación a la función addShape
y la creación de figuras compuestas de manera directa.
Por ejemplo podrías añadir figuras compuestas al Panel sin ningún problema de la siguiente forma:
Estamos muy cerca del objetivo final. Solo nos falta poder combinar figuras con los operadores +
y -
, siendo el operador +
correspondiente a la unión y el operador -
correspondiente a la intersección. En el próximo artículo haremos eso además de una pequeña optimización.
Continúa en el siguiente artículo con Construcción del DSL: 5 — Los operadores ‘plus’ y ‘minus’ y funciones ‘inline’
- 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