Rekomendo! The Making of. Parte 1: Backend

Hace algo más de diez meses comencé un proyecto de recomendación cultural en forma de aplicación móvil llamado Rekomendo! A día de hoy se encuentra disponible para plataformas Android, iOS y web, y después de todo este tiempo he decidido escribir otro post, esta vez sobre cómo lo he hecho.

API

Tras haber madurado un poco la idea inicial del proyecto me puse manos a la obra y, ya que tenía claro que iba a realizar una aplicación móvil, necesitaría una API para, a través de ella, almacenar, consultar y manejar la información. Durante unos días me planteé la opción de utilizar Go. Pero como no tenía mucha experiencia, y realmente lo que quería era ser productivo para sacar el proyecto adelante y no morir en el intento, me decanté por PHP (7), el lenguaje con el que más experiencia profesional tengo.

Como framework escogí Silex porque me gustan los micro-frameworks y porque ya lo tenía controlado. Ahora mismo, desafortunadamente para mí, se ha marcado como deprecated por Sensio Labs (sus creadores), en favor de Symfony4. En cualquier caso tenía claro como separar el framework del resto de la lógica de la aplicación (con un poco de arquitectura hexagonal), así que será relativamente fácil reemplazarlo en un futuro.

Empece a programar el API en formato RESTful siguiendo mi propia especificación en Swagger. Dicha especificación me proporcionó una documentación robusta y fiable. Así nunca me olvidaría de los parámetros, formatos y tipos de respuesta a medida que iba desarrollando nueva funcionalidad. Además, utilizando el Swagger UI podía ir probando manualmente mi API (a modo de debug) de una forma cómoda y sencilla.

La especificación de la API en Swagger me permitió también obtener el código de los clientes que la consumirían de forma automática sin escribir ni una sola línea. Con una herramienta llamada Swagger CodeGen se pueden auto-generar código en una amplia variedad de lenguajes a partir de dicho documento formal. Así pude obtener (y actualizar a medida que mi especificación iba evolucionando) un cliente en TypeScript-Angular2 (que utilizo desde el frontend) y otro en PHP (que sirvió para crear una aplicación sencilla para generar datos de prueba).

Tests

Tuve claro desde el principio que el testing era imprescindible. No era una opción. Quería escapar de ese concepto utópico que representa el MVP. Desde mi experiencia, los MVPs siempre acaban siendo el producto final y nunca jamás se rehacen. Un MVP se agarra al entorno de producción como un koala. Bajarlo del árbol para reemplazarlo por una nueva y flamante versión mejorada no es algo muy frecuente. No quise caer en esa trampa. Sabía que si no me cubría de tests y buenas prácticas en el diseño desde el principio (hasta donde dan mis pocos conocimientos prácticos de DDD) llegaría un punto en el que todo empezaría a escaparse a mi control. Necesitaba asegurarme de que lo que ya había escrito seguiría funcionando, y podía ser reemplazable. No quería pasarme el día arreglando bugs, sino generando nueva funcionalidad. Al fin y al cabo, eso es lo divertido, ¿No?.

Me propuse hacer unos cuantos tests unitarios con PHPUnit (aquellos que creía necesarios sin importarte el coverage) para algunas clases cuya implementación no era del todo trivial. Los UTs me ayudaron en todos esos casos a desarrollar más rápido.

Escribí (y lo sigo haciendo) muchos test de integración con Behat, en lenguaje Gherkin, para la capa de Dominio y Aplicación. Así me aseguraba que la lógica de negocio funcionaba, sin tener en cuenta la interfaz de comunicación con el backend de mi App (en este caso el API REST). Otra ventaja es que a medida que los iba creando, iba generando documentación del proyecto en lenguaje natural que estoy seguro me resultará de gran ayuda cuando mi memoria empiece a fallar.

Por último, creé tests para probar cada método HTTP disponible de mi API a modo de tests de aceptación. Gracias a la especificación de Swagger pude importar la documentación en Postman y así definir lo que ellos llaman un Test Runner. Un Test Runner representa la automatización configurable de una serie de llamadas HTTP realizadas secuencialmente, con la capacidad para comprobar si la respuesta obtenida es la esperada o no. Por ejemplo, si envío un POST a /users/ con el payload adecuado el API me responderá un 204 (todo ha ido bien, el test pasa), en cambio si la información contenida en la petición no es válida el código HTTP devuelto será diferente (algo ha ido mal, el test fallará). Para peticiones GET me permite además comprobar el contenido de la respuesta (no solo el de las cabeceras), si hago un GET a /users/pepe podré asegurarme de que en el contenido de la respuesta el campo “nombre” tiene como valor “pepe”, sino es así, el test también fallará.

Es muy sencillo ejecutar un Test Runner desde la interfaz gráfica de Postman, pero también lo es desde consola gracias a Newman. Solamente es necesario exportar la información del Test Runner desde Postman a un archivo JSON, que será precisamente el input de Newman (junto con otro archivo de configuración de variables). Esto era importante porque así podía ejecutar estos test de “aceptación” desde Travis con cada push a master (intentado acercarme a un modelo de integración continua).

Persistencia de Datos

De haberlo sabido antes, me hubiese inclinado a utilizar Firebase como solución de persistencia de datos de la aplicación. Al menos, seguro, la parte de gestión de usuarios. El caso es que utilicé MySQL para todo.

Una de las ventajas de Rekomendo! es que no guarda ningún tipo de información sobre los elementos que puedes encontrar en el catálogo, como películas, series, música, libros, videojuegos, etc… bueno en realidad sí, solamente se guarda el identificador único de cada uno de ellos para establecer ciertas relaciones, como por ejemplo las reviews que realizan los usuarios.

En cambio, los metadatos sobre los elementos se obtienen de APIs públicas externas a la app. Por ejemplo, para la música utilizo la API de Spotify, para videojuegos la de IGDB, para libros Google Books API, y así sucesivamente. Las voy consultando en tiempo real, a medida que los usuarios hacen alguna búsqueda o visitan alguna que otra sección de la aplicación.

Una vez recibida la información la normalizo y la almaceno durante un tiempo en Redis. Así, además de poder responder muchísimo más rápido en sucesivas peticiones, no corro el riesgo a que dichas APIs públicas me bloqueen por sobrepasar el número permitido de requests. En definitiva, uso Redis como una caché.

El hecho de normalizar estos datos es importante para poder añadir una nueva colección de elementos de forma sencilla. Es decir, si quiero añadir una nueva sección, por ejemplo “cervezas”, solo haría falta encontrar una API pública a la que poder llamar para obtener los metadatos de los nuevos elementos y crear un pequeño adaptador específico que normalizara la información que llega de esa fuente. No necesitaría realizar ningún otro cambio en el resto de la lógica de la aplicación.

Motor de Recomendaciones

El corazón de Rekomendo! son, evidentemente, las recomendaciones. Para ello utilizo una Base de Datos orientada a grafos llamada Neo4J. Esta herramienta se diferencia de una Base de Datos tradicional en que en lugar de almacenar la información en tablas, lo hace en grafos.

Un nodo está representado por el usuarios Pepe, de ese nodo salen unas aristas que lo relacionan con otros nodos que son películas, series, libros, etc… a su vez esos elementos reciben más aristas de otros nodos usuarios, y los nodos usuarios se conectan también con otros nodos usuarios (followers), y así se va generando el grafo con todas las relaciones y nodos disponibles.

Gracias a su lenguaje de consulta llamado Cypher pude responder preguntas que serían bastante más complejas y costosas de realizar con otros sistemas gestores de Bases de Datos, algunos ejemplos serían ¿Si a Pepe le gusta esta película, que otras películas es probable que le gusten en función de la gente a la que sigue? ó Dado un libro ¿Qué otros libros, series o videojuegos les han gustado a los usuarios que han votado con una buena puntuación dicho libro?

Es muy sencillo generar un modelo mental utilizando Neo4J y traducirlo en su lenguaje de consulta. Además, he podido comprobar que es tremendamente rápido en responder, bien es cierto que consume muchos recursos en el servido (es una aplicación Java).

La forma en la que conecto Neo4J con el resto de la aplicación es muy sencilla. Cuando algo pasa en mi sistema y quiero registrar la información en Neo4J, publico un evento que hace que se actualice el grafo. Cuando se registra un nuevo usuario, además de guardar la información en MySQL, también se genera un nuevo nodo en el grafo. Y cuando ese usuario vota un juego de mesa, creo una arista hacia el nodo representado por ese juego, asignándole la votación en el peso de dicha conexión.

En resumen, el motor de recomendaciones es tan sencillo como efectivo. Se basa en relaciones directas (o indirectas) entre nodos del grafo.

Rekomendo! está disponible para dispositivos Android, iOS y también cuenta con una versión web.

Like what you read? Give Rubén Cougil Grande a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.