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:

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.

Operaciones Unión e Intersección

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…

Código inicial

…genera la siguiente salida en consola:

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 final

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:

Shapes-DSL: Estructura de directorios

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ámetro lines , sin embargo, decidí utilizar la variable center. 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.

--

--

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.