Kotlin DSL | Código Base: Proyecto Shapes-DSL
Presentación del código inicial
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 este artículo te presentaré el código base sobre el cual construiremos un DSL. En caso de que te hayas saltado los artículos anteriores, debes saber que para la construcción de un DSL es imperativo que conozcas algunas de las características de Kotlin que son aplicadas intensivamente. Estas características son:
- Lambdas
- Funciones de orden superior
- Funciones de extensión
- Lambdas con receptor
- Funciones de ámbito
- El patrón de diseño ‘Constructor’
Además, durante la construcción del DSL también se aplicarán otras características que permiten un resultado más idiomático. Estas características son:
Si no conoces alguna de las características de Kotlin anteriores, te sugiero hacer clic sobre ella y echarle un vistazo antes de continuar.
Descripción del proyecto
El código que te mostraré en este artículo consiste en una aplicación de consola que imprime figuras geométricas. Concretamente serán 4 figuras geométricas: Cuadrado, Triángulo, Rombo y Figura Compuesta.
Las figuras tendrán la capacidad de ser combinadas por medio de las operaciones Unión e Intersección formando así una figura compuesta. Estas operaciones son análogas a las operaciones entre conjuntos en matemática. La operación Unión formará una figura compuesta por los espacios que ocupa alguna de las figuras o las dos figuras involucradas. La operación Intersección formará una figura compuesta por los espacios en común ocupados por ambas figuras involucradas.
La idea es agregar figuras geométricas de diferentes tamaños en un Panel e imprimirlas todas, una al lado de otra, línea por línea, simulando el funcionamiento de una impresora física que imprime sobre papel.
Por ejemplo, el siguiente código…
…genera la siguiente salida en consola:
El objetivo es construir un DSL sobre este código de manera que la creación del Panel, así como la creación de las figuras y su inclusión dentro del Panel, se haga de una forma más idiomática:
Código Base
A continuación presentaré el código inicial y aunque es bastante cortito y sencillo te sugiero seguir con la lectura para que estés seguro de que no se te ha pasado ningún detalle.
🔗 Puedes encontrar el código inicial en mi cuenta de GitHub en el siguiente enlace: Shapes-DSL
Asegúrate de descargar el proyecto y de hacer un checkout hacia la rama master
si no estás en ella. En la rama master
se encuentra el código base el cual no será modificado bajo ninguna circunstancia a excepción del método main
dado que nuestra intención es cambiar ese tipo de sintaxis por una más idiomática con nuestro DSL.
La rama dsl
contiene el código final al que llegarás al concluir esta serie de artículos. El objetivo final es que aprendas a construir un DSL sobre código existente, especialmente sobre código que no nos es posible modificar ya sea porque es código que forma parte de un módulo del que no tienes acceso o permiso de modificar, o porque viene empaquetado en librerías externas. A la vez tendrás una idea de cómo están construidas muchas de las librerías que descargamos y usamos en nuestros proyectos tales como Koin y Ktor.
Estructura de paquetes
Si abres el proyecto y despliegas la lista de directorios bajo el apartado Project, observarás la siguiente estructura de árbol:
Bajo el paquete console_shapes
encuentras 2 paquetes: container
y shapes
. El paquete container
contiene solamente la clase Panel
que asumirá el rol de contenedor de las figuras geométricas. El paquete shapes
contiene todas las clases que representan a las figuras geométricas que pueden ser agregadas al Panel.
El paquete ‘Shapes’
El proyecto cuenta con 4 figuras geométricas de las cuales 3 son figuras simples y 1 es compuesta. Las figuras Square
, Triangle
y Rhombus
corresponden a las figuras geométricas simples. La figura ComposedShape
corresponde a la figura compuesta ya que solo se puede construir a partir de la combinación de 2 figuras, ya sean ambas simples, ambas compuestas o una simple y una compuesta. Adicionalmente tenemos la figura Space
que representará un espacio en blanco dentro del Panel.
- Shape.kt
La clase abstracta Shape
es la clase base que define el comportamiento común de todas las figuras geométricas.
La constante WHITE_SPACE
define el caracter que tomará lugar en cada espacio vacío de la figura.
La propiedad grid
define una matriz de caracteres — array de CharArray — y representa la figura dentro del Panel. La matriz será de un determinado alto y ancho que se construirá según el número de líneas que tenga la figura. Cada clase que extienda a Shape
definirá su propio algoritmo que dimensiona y rellena su propia matriz con WHITE_SPACE
y con el caracter que dicha clase determine.
La propiedad line
define un lambda con acceso personalizado — custom accessor — sin campo de respaldo — backing field — que retorna un CharArray
según el índice de la fila recibida como parámetro de entrada.
La propiedad size
define una variable con acceso personalizado — custom accessor — sin campo de respaldo — backing field — que retorna el número de filas de la matriz correspondiente a la propiedad grid
.
🔗 Si estás iniciando con Kotlin y aún no comprendes muy bien la notación y el funcionamiento de los accesos personalizados y/o los campos de respaldo, puedes echarle un vistazo al apartado Getters and setters de la documentación oficial de Kotlin en el siguiente enlace: https://kotlinlang.org/docs/properties.html#getters-and-setters
- Square.kt
La clase Square
representa a la figura geométrica ‘Cuadrado’ que extiende a la clase abstracta Shape
.
Sobrescribe la propiedad grid
creando una matriz de caracteres con un número de filas y número de columnas determinados por el parámetro lines
.
Durante su construcción se reciben los siguientes parámetros:
— lines
: Determina el número de filas de la matriz de caracteres.
— char
: Determina el caracter utilizado para rellenar la matriz de caracteres.
- Triangle.kt
La clase Triangle
representa a la figura geométrica ‘Triángulo’ que extiende a la clase abstracta Shape
.
Sobrescribe la propiedad grid
creando una matriz de caracteres cuyo número de filas será determinado por el parámetro lines
. El número de columnas lo determinará la variable width
en función del parámetro lines
.
Durante su construcción se reciben los siguientes parámetros:
— lines
: Determina el número de filas de la matriz de caracteres.
— char
: Determina el caracter utilizado para rellenar la matriz de caracteres.
- Rhombus.kt
La clase Rhombus
representa a la figura geométrica ‘Rombo’ que extiende a la clase abstracta Shape
.
Sobrescribe la propiedad grid
creando una matriz de caracteres cuyo número de filas será determinado por el parámetro lines
. El número de columnas lo determinará la variable width
que se calcula en función de la variable center
que a su vez se calcula en función del parámetro lines
.
💬 La variable
width
podría calcularse directamente en función del parámetrolines
, sin embargo, decidí utilizar la variablecenter
. Algunas veces suelo sacrificar rendimiento por claridad en el código, pero solamente cuando me parece que la pérdida de rendimiento es inocua. No es lo mismo notar claramente que la anchura de un rombo está en función de su centro, que ver su anchura en función de su altura.
Durante su construcción se reciben los siguientes parámetros:
— lines
: Determina el número de filas de la matriz de caracteres.
— char
: Determina el caracter utilizado para rellenar la matriz de caracteres.
- ComposedShape.kt
La clase ComposedShape
es un tipo especial de Shape
ya que no establece una forma definida, sino que su forma se construye en función de las dos figuras que recibe como parámetros en su constructor aunadas al parámetro operation
que determina la operación que se les debe aplicar a dichas figuras.
El enumerado Operation
define las dos acciones — ilustradas anteriormente— que se pueden realizar al combinar 2 tipos de formas:
UNION
: formará una figura compuesta por la unión de las matrices de caracteres de las figuras involucradas en la operación.INTERSECTION
: formará una figura compuesta por la intersección de las matrices de caracteres de las figuras involucradas en la operación.
Sobrescribe la propiedad grid
asignándole la matriz de caracteres recibida por la función que invoque según la operación determinada por el parámetro operation
.
Define las funciones privadas union
e intersection
, ambas funciones de extensión de la clase Shape
que combinarán las matrices de caracteres de las dos figuras involucradas. Las operaciones las realizará fila por fila y el resultado de cada operación estará sujeto al valor devuelto por las funciones de extensión de CharArray
; union
e intersection
.
Define las funciones privadas union
e intersection
, ambas funciones de extensión de la clase CharArray
, para realizar la combinación entre las filas correspondientes de las figuras involucradas. El resultado de la operación estará sujeto a la acción que debe realizar y que ya ilustré anteriormente.
- Space.kt
Esta clase definirá un objeto único aplicando el patrón de diseño Singleton. Extiende a la clase Shape
para que pueda ser agregado en el Panel de figuras geométricas. Este objeto representa un espacio en blanco y servirá para separar las figuras dentro del Panel. La razón de aplicar el patrón de diseño Singleton es porque el objeto no varía en su composición y resulta más caro crear un objeto cada vez que se requiera un espacio en blanco siendo posible reutilizarlo donde sea necesario.
Define la constante privada WIDTH
que determinará la cantidad de columnas que tendrá la matriz de caracteres.
Sobrescribe la propiedad grid
devolviendo una matriz de caracteres vacía.
Sobrescribe la propiedad line
correspondiente al lamba que retorna un CharArray
según el índice de la fila recibida como parámetro de entrada. El CharArray
retornado estará compuesto de caracteres determinados por la constante WHITE_SPACE
y tendrá una longitud determinada por la constante WIDTH
.
El paquete ‘Container’
Existe solamente una clase cuyo rol es contener una colección de figuras geométricas para imprimirlas en consola.
- Panel.kt
Define la propiedad shapes
que corresponde a la lista de figuras que contiene el Panel.
Define la función addShape
que recibe un figura como parámetro de entrada y la agrega a la lista de de figuras.
Define la función print
que imprime en consola todas las figuras de la lista, una fila a la vez.
Sin duda el proyecto Shapes-DSL podría implementar más figuras geométricas y hasta podría ser depurado un poco más. Por ejemplo, podríamos establecer una restricción para que el objeto Space
no pueda ser usado como una de las figuras para crear un ComposedShape
, sin embargo, el objetivo de esta serie de artículos es la creación de un DSL sobre código existente asumiendo que no tenemos el control del código base.
Vamos a dejar el código base tal como está y en el siguiente artículo iniciaremos la construcción del DSL.
Continúa en el siguiente artículo con Construcción del DSL: 1 — Organización de paquetes y el objeto ‘Panel’
- Puedes acceder al código fuente presentado en este artículo desde mi cuenta de GitHub en el siguiente enlace: https://github.com/pencelab/Shapes-DSL