Pragmatic Types
Quiero modelar el catálogo de una tienda de ropa. Empezaré con algo muy simple: cada prenda de ropa está identificada por un número entero, y de cada prenda se conoce su modelo, el precio, una descripción y la fecha con la que salió por primera vez a la venta.
|
|
Esta información se puede representar con la siguiente tabla en una base de datos relacional:
|
|
Como la correspondencia entre el modelo de negocio y el relacional es directa no necesito pasar por una representación intermedia. Por ejemplo, el siguiente código usa doobie para recuparar una prenda de la base de datos mediante su identificador:
|
|
En el mismo fragmento de código se mezcla la consulta escrita en SQL con la respuesta que utiliza la clase Garment de mi modelo de dominio.
Primer cambio: diferentes monedas#
Ahora quiero mejorar la experiencia de usuaria, y para eso voy a modificar el catálogo para que cada usuaria pueda ver los precios en la moneda con la que está más familiarizada.
Para este cambio me voy a apoyar en Squants, un sistema que permite operar con cantidades y sus unidades de medida. Voy a cambiar el modelo para representar el precio como Money, un tipo de cantidad dimensional cuyas unidades de medida son las monedas:
|
|
Para llevar este cambio a la base de datos necesito decidir cómo se van a guardar los precios:
- Podría almacenar cada precio con su moneda, pero eso dificultaría las búsquedas por rango de precio y también sería más difícil ordenar por precio.
- Otra opción sería guardar todos los precios en las diferentes monedas, pero la tasa de cambio entre monedas podría cambiar y sería muy difícil hacer todas las actualizaciones.
- Finalmente, lo que voy a hacer es guardar todos los precios en la misma moneda y voy a hacer los cambios de moneda en la aplicación, aplicando la tasa de cambio vigente.
Así pues, el único cambio que voy a hacer en base de datos es renombrar la columna price a price_in_eur:
|
|
Sin embargo, este pequeño cambio me obliga a repensar cómo voy a traer los datos del modelo relacional a mi modelo de negocio. En mi base de datos el precio es una cantidad sin dimensiones, mientras que en el modelo cada precio está acompañado de su moneda.
Para representar esta diferencia voy a introducir la clase GarmentRow que representa una entrada en la base de datos:
|
|
Además de contener los datos de la prenda tal y como están en la base de datos, esta clase sabe aplicar las transformaciones necesarias para llevar los datos al modelo de negocio. En este caso, la clase sabe transformar el precio a la moneda con la que quiero trabajar en cada momento.
Para esta transformación me he apoyado en la librería ducktape, que permite transformar entre dos case classes centrándonos solamente en aquellos campos que son diferentes.
Además, la transformación necesita que haya un MoneyContext que defina cuál es la moneda con la que quiero trabajar (en este caso EUR porque es la moneda en la que están almacenados los precios en la base de datos):
|
|
También he aprovechado el MoneyContext para definir las tasas de cambio entre las monedas soportadas por mi aplicación. De esta forma puedo cambiar la moneda de la prenda utilizando las tasas de cambio que están definidas en el contexto:
|
|
Segundo cambio: impuestos#
Además de tener diferentes monedas, los países aplican diferentes impuestos a la comercialización de bienes:
|
|
Generalmente, los impuestos se aplican a diferentes grupos de bienes o servicios. Por ejemplo, la mayoría de los bienes pagan el tipo general, mientras que el tipo reducido se aplica a algunos bienes o servicios considerados de primera necesidad. Por el contrario, otros bienes son considerados de lujo y por eso se les aplica un tipo más elevado.
Teniendo en cuenta lo anterior, voy a crear en la base de datos el tipo sales_tax para representar los tipos impositivos de impuestos:
|
|
Además, he creado la tabla taxes que define cuánto se paga por cada tipo. Por ejemplo, 21% por el tipo general, 10% por el tipo reducido, etc:
|
|
Con estos cambios puedo hacer que cada prenda esté anotada en base de datos con el tipo impositivo de impuesto que se le aplica:
|
|
De esta forma, si quiero recuperar una prenda mediante su identificador como hacía la función findGarmentById introducida anteriormente, tendría que hacer una JOIN con la tabla de taxes para saber cuánto se paga por el tipo de impuesto:
|
|
Al igual que con las monedas, este cambio me obliga a repensar el diseño de las salidas de la base de datos, y demás, esta vez también me va a obligar a pensar en las entradas. Vayamos paso a paso.
Antes había definido la clase GarmentRow para representar una entrada en base de datos, y me servía tanto para insertar una prenda nueva como para recuperar la información de una prenda de la base de datos.
Con la introducción de los impuestos necesito dos clases diferentes. La clase GarmentDbOut me va a servir para recoger la salida de la JOIN definida más arriba:
|
|
Para introducir nuevas prendas en la base de datos voy a utilizar la nueva clase GarmentDbIn junto con la enumeración SalesTax, que refleja el nuevo tipo sales_tax creado en la base de datos:
|
|
Primera mejora: tipos refinados#
Hasta ahora he utilizado tipos primitivos en mi modelo de negocio, con la única excepción del tipo Money que utilicé para representar el precio. Sin embargo, en la base de datos he ido introducciendo restricciones a los tipos. Por ejemplo, taxes.rate es un número entre 0 y 1, garments.price_in_eur es positivo distinto de cero, etc.
Esto quiere decir, que mi modelo relacional está más refinado que mi modelo de negocio. Voy a hacer que las restricciones coincidan en ambos modelos.
Con la ayuda de la librería Iron he definido las restricciones que me permiten refinar los tipos que utiliza mi modelo de negocios:
|
|
Por ejemplo, el tipo UnitFraction se define como un intervalo cerrado entre 0 y 1:
|
|
De esta forma, cada uno de los campos de mi modelo queda tipado y refinado.
Tercer y último cambio: ahora vendemos electrodomésticos#
El negocio se expande y además de la ropa quiero ofrecer nuevos tipos de productos en mi tienda online. Para empezar, voy a añadir electrodomésticos al catálogo:
|
|
El modelo de negocio es muy parecido, y tiene algunos campos que coinciden con la ropa, como el precio y los impuestos. Además, la representación en base de datos aprovecha lo que ya había hecho hasta ahora:
|
|
Habiendo llegado a este punto, lo más lógico sería definir abstracciones para trabajar con los dos tipos de productos que tenemos hasta ahora: ropa y electrodomésticos.
Segunda mejora: abstracciones#
Para mi catálogo necesito una operación que me permita filtrar productos los campos más relevantes, y también ordenar los listados de productos que generen estos filtros. Por ejemplo, una usuaria puede estar interesada en los Smart TV que tengan un consumo por debajo de los 150 W, y además quiere verlos ordenados por precio ascendente.
De forma semejante, otra usuaria puede estar interesada en los pantalones vaqueros de nueva temporada.
En resumen, necesito una forma de deninir qué campos se pueden utilizar para filtrar y cuáles para ordenar cada producto.
Filtrado#
Para el filtrado voy a definir los filtros, que pueden ser combinaciones (And, In, Or, Not) o comparadores (Between, Equal, Greater, Less, Like):
|
|
Cada filtro recibe los argumentos que necesita. Por ejemplo, el filtro And recibe un listado de filtros que se combinan con la operación lógica AND:
|
|
Algunos filtros reciben un tipo de argumento con un context bound de tipo Put que además necesita un tipo Filterable en el contexto:
|
|
El tipo Put no es más que una transformación de doobie entre el tipo T y el tipo equivalente en base de datos. En cambio, Filterable es una abstracción propia que marca aquellas columnas que se pueden utilizar para filtrar los datos. Aprovechando la librería Tagging he marcado las columnas con una etiqueta que evita que se puedan construir filtros que no están soportados por la aplicación:
|
|
Además, cada filtro define la operación sql que transforma el filtro en una secuencia de operaciones en lenguaje SQL. La enumeración ColumnFormatter permite definir si las columnas utilizan su nombre simple (e.g. priceInEur) o su nombre completo (e.g. garments.priceInEur):
|
|
Ordenación#
La ordenación puede ser ascendente o descendente, y al igual que los filtros definen una operación sql para transformar la ordenación en un fragmento válido en SQL:
|
|
Como en el caso de los filtros, hay una etiqueta Sortable que marca aquellas columnas que pueden ser utilizadas para la ordenación:
|
|
Juntándolo todo en una tabla#
Una tabla es una abstracción que define un tipo en base de datos, junto con sus operaciones de inserción, búsqueda por identificador y creación de listas filtradas y ordenadas:
|
|
Cada tabla tiene 3 parámetros de tipo:
- El
Idcon los context bound de tipoPutyFilterableque hacen posible la operación de búsqueda porId(findBy). - El
Inputque es el tipo que utiliza la inserción. - El
Outputcon el context bound de tipoReadque es el equivalente dePutpara los tipos con múltiples columnas.
El Input y el Output pueden ser iguales o diferentes, según se necesite. También se puede usar un modelo de negocio si no fuera necesario hacer ninguna transformación.
Ejemplo final#
Así quedaría por ejemplo, la clase GarmentTable que define en mi catálogo las operaciones para trabajar con prendas de ropa:
|
|
Todas las columnas reciben el nombre de la tabla del Table.Name implícito en el contexto. Se puede filtrar la ropa por el identificador, el modelo, el precio y la fecha de lanzamiento. Las columnas que se pueden utilizar para ordenar son el precio y la fecha de lanzamiento.
Se han reescrito las operaciones de búsqueda por identificador y la creación de listados filtrados y ordenados para recuperar el valor del impuesto que aplica a cada prenda.
Cierre#
He creado un modelo con tipos refinados que luego he aprovechado para crear abstracciones que han terminado convirtiéndose en un DSL para insertar, seleccionar y crear listados de productos en un catálogo.
Me parece un buen punto de partida sobre el que seguir iterando. En especial me gustaría encontrar un modo de ocultar los detalles de la base de datos durante la creación de los filtros (evitar el context bound de tipo Put).
El código completo del ejemplo está disponible en GitHub1.
-
Foto de Deon Black en Unsplash ↩︎