Desarrollando un MVP: PHP y Clean Architecture

Antes de entrar al mundo corporate, estuve trabajando en el MVP de un nuevo producto con la buena gente de Antevenio.

Con este MVP pude cerrar con buen sabor de boca mi trayectoria como desarrollador independiente de 10 añazos (sumando el tiempo de andar por mi cuenta como dentro de Coding Stones), así que me apetecía escribir sobre ello.


El tiempo

El MVP debía estar disponible en un par de meses, por lo que la gestión del alcance y priorización del backlog eran críticos. Con alguien como Rubén Orta ejerciendo como product manager, resultó fácil poner en común y coordinar ambas cosas.

A partir de los prototipos y documentación de requisitos, extrajimos las historias de usuarios para volcarlas en un user story map. Principalmente para organizar las releases internas en varios swim lanes (finalmente varias historias que estaban en el nice to have se tuvieron que quedar fuera del alcance del MVP).

Con tan poco tiempo teníamos que obtener feedback temprano, para controlar cualquier desvío e ir validando la aceptación de las historias de usuario desarrolladas. Así que la propuesta fue hacer iteraciones semanales, para al final de cada una hacer la demo y ver las siguientes historias en las que trabajar.


La arquitectura

Como en Antevenio tienen mucha experiencia tanto en sus equipos de desarrollo como en producción, a nivel técnico había 2 requisitos que se debían cumplir sí o sí: Que estuviera desarrollado en PHP y usar MySQL como gestor de bases de datos para la persistencia.

Los que ya habéis leído algún post por aquí, supongo que seréis conscientes de que el enfoque en nuestros desarrollos fueron mayoritariamente hacer aproximaciones de Hexagonal/Clean Architecture y Domain Driven Design. Nos gusta hacer software pensado para que dure: desarrollado centrado en el negocio minimizando los puntos de dependencia a frameworks/tecnologías, que sea flexible pudiendo absorber cambios con relativa facilidad y desde luego que esté bien testeado. Al llevar mucho tiempo sin programar profesionalmente con PHP, estaba todavía más claro que también iba a ser la opción este caso.

Dentro de la circunferencia en negrita son todo clases PHP, sin dependencias a librerías

También decidimos el uso de Slim como framework web, un microframwork que además de la gestión de rutas, renderización de vistas, etc. también ofrece inyección de dependencias. Iba a resultar más natural para nuestra colaboración que un framework que ofreciera mucho más (y me costara bastante más de aprender) como Symfony.


La documentación

Fui haciendo el ejercicio de documentar algunas de las decisiones que había tomado en el transcurso del proyecto, incluyendo algo de deuda técnica de la que era consciente junto con alguna propuesta de cómo ir pagándola.

Y como pienso que puede resultar de utilidad, he hecho el ejercicio de limpiar las referencias al dominio o diseño en concreto, además de alguna que otra edición para que tenga más sentido sin la posibilidad de consultar el código.

La aplicación ha sido desarrollada utilizando patrones tácticos de Domain Driven Design.
De fuera hacia adentro: Application Services, Domain Services, Repositories y Aggregates formados por Entities y Value Objects.
De modo que combinando esto con una aproximación de ports & adapters (arquitectura hexagonal), la lógica de negocio está completamente desacoplada de la infraestructura, que son detalles de implementación.
El framework web utilizado es Slim y para la persistencia se utiliza PDO, además de algunas otras librerías con menor impacto que se pueden consultar en las dependencias de composer. Al utilizar ports & adapters, se ha evitado la dependencia a cualquiera de esas librerías más allá de los propios adapters o el uso desde Slim del core de negocio.
La distribución de directorios está organizado de este modo: src, db, templates, tests y public.

src

Como es de suponer, en src tenemos el grueso de lo que nos interesa, dentro de ese directorio tenemos la siguiente estructura:
Actions. Que son los Application Services, puntos de entrada al sistema de cómo se interactúa con él y orquestadores con el mundo exterior.
A su vez los directorios están organizados por unidad lógica con el nombre en plural. Y los nombres de las clases son (o deberían ser) autoexplicativos. Hay definido un BaseAction con intención de decorar las acciones para cuestiones como la auditoría y la transaccionalidad de lo que ocurre dentro de cada acción.
Podréis encontrar algunas diferencias entre acciones pertenecientes a Write Model y Read Model.
Las de Write Model trabajan puramente con repositorios, entidades, etc. ya que es donde ocurre el dominio. Mientras que en algunas de Read Model devolvemos instancias de clases con el “apellido” Projection, que simplemente son DTOs inmutables que devuelven una proyección del estado del dominio.
Actualmente las projections se componen a través de los datos traídos de los repositories, lo cual no es óptimo por el antipatrón de queries N+1, así que se podría modificar eso pronto para hacer joins; tal vez con una abstracción con algún apellido tipo Query para dotarle de cambiabilidad (una vista, otra base de datos con otro esquema, otra tecnología de persistencia…) sin tener que tocar el Action.
Infrastructure. Es el mundo exterior, lo que no podemos testear de forma unitaria: el tiempo del sistema, un generador de ids, utilidades para trabajar con el dom… también podrían haber caído aquí las implementaciones concretas de las interfaces de los patrones Repository, seguramente sea mejor sitio que en el directorio Model.
Model: Es el core formado por las reglas de negocio y entidades de dominio. En este caso los directorios están organizados con el nombre en singular del Aggregate Root y dentro las clases Entities, Value Objects, Domain Services y Repositories (tanto interfaces como implementaciones).
Como se puede comprobar, las entidades no mapean 1:1 a la persistencia relacional como sería utilizando un ORM, sino que en las implementaciones de los Repository hacemos el trabajo de mapeo con la persistencia.
Evidentemente podrían implementarse otros Repository con cualquier ORM o toolkit para evitar el código repetido actual, incluso podrían pasarse a una base de datos no relacional en un momento dado.
App.php: Es el framework Slim. Es un fichero bastante grande (más de 300 líneas) al tener mezcladas las responsabilidades de la inyección de dependencias y la definición de las rutas y su mapeo con sus respectivas Actions.
Propongo partir ambas responsabilidades, e incluso partir la definición de rutas de zonas público y privada.

db

En db tenemos las migraciones de la base de datos, esto significa que tenemos versionado el esquema de la base de datos, facilitando su sincronización y puesta al día.
No es que resuelva todos los problemas, pero es un paso que puede ahorrar muchos dolores de cabeza. En este caso se está utilizando phinx.

Templates

En el directorio templates tenemos las plantillas html de la UI. Esas plantillas apenas tienen html, por no generar código tontamente que luego pueda molestar para meterle el diseño/maquetación final.

tests

En tests está replicada la misma estructura que en src: Actions / Infrastructure / Model.
En Actions tenemos una mayoría de tests unitarios de las acciones, de modo que se testea desde ahí las interacciones y reglas de negocio.
Aunque para algunas acciones de Read Model son tests de integración, ya que así no tenemos tanto acoplamiento teniendo que doblar a sus colaboradores, son algo más lentos pero en esos casos vi que todo el andamiaje de usar dobles no aportaba demasiado.
En Infrastructure son todo tests de integración, como se puede ver en la implementación son tests muy generales que prueban sólo casos que nos den seguridad de que funciona comprobando las salidas para unas entradas dadas.
En Model tenemos los tests de los Repository, que son de integración también. De estos comprobamos un contrato, para unas entradas esperamos unas salidas. De modo que aún cambiando la implementación de la tecnología de persistencia esos tests nos deberían valer, sólo deberíamos cambiar lo relacionado con el setup del system under test.
En AppTest.php comprobamos a nivel de integración con framework el happy path y algunos casos de error con llamadas http a las rutas. Así tenemos bastante seguridad de que las piezas juntas funcionan a través de la inyección de dependencias, además de saber que las referencias a las plantillas html son correctas.
Esos tests automáticos son los de más alto nivel de la pirámide de testing, no hay implementados tests end 2 end desde un navegador.

public

En public simplemente están los assets y ficheros de configuración que inicializan la aplicación en diferentes entornos.

Las conclusiones

  • Siempre es clave la alineación negocio/producto/desarrollo para tratar de sacar productos de software con éxito. E insisto, en un proyecto con tan poco margen de maniobra, la capacidad de jugar con el alcance y la priorización son críticas para evitar gastar energías en balde.
  • Además el producto se estuvo desarrollando siguiendo TDD con estilo Outside-In (empezando por las Actions que representan cada feature). Lo que ayuda a evolucionar rápido y con pasos cortos, donde va emergiendo el diseño, además de ir generando de paso una red confianza para introducir cambios en el futuro.
  • Aún con la falta de práctica diaria de utilizar PHP, me atrevería a decir que la productividad a la hora de sacar funcionalidad no se vio mermada al seguir esta aproximación, además de evitar comprometer la evolución y futuro del producto.

¡Nos vemos! :)