Implementando DDD y Arquitectura Hexagonal en PHP Con Laravel
Implementando DDD y Arquitectura Hexagonal en PHP Con Laravel
Por estos motivos, por mi pasión por las arquitecturas y los patrones de diseño de
software, por el código limpio y legible, y porque en el último periodo he tenido la
oportunidad de trabajar en proyectos desarrollados en PHP que utilizaban como
framework Laravel, he decidido escribir este artículo.
Existe mucha información que trata de los aspectos tácticos del Domain Driven Design
(DDD) y también mucha relativa a la implementación de clean architectures o
arquitecturas limpias en PHP, pero muy poca que lo reúna todo y lo aplique a Laravel que
como framework PHP está teniendo cada vez más auge debido a la facilidad con la que
puedes desarrollar aplicaciones en tiempos muy rápidos y con un código elegante.
¿Pero porque si un proyecto tiene una perspectiva a medio/largo plazo debería utilizar
Laravel y no un framework como Symfony por ejemplo?
Una de las posibles respuestas podría ser porque no todos los proyectos tienen esa
perspectiva al principio, a menudo empiezan siendo pequeños y por eso necesitan ser
desarrollados rápidamente y Laravel es perfecto para esos casos.
Pero con el tiempo, pueden llegar a ser cada vez más grandes y convertirse en un negocio
que funciona y necesita de mantenimiento y escalabilidad a medio/largo plazo y claro, no
todo el mundo se puede permitir refactorizar todo de nuevo con los costes de tiempo,
know-how y mano de obra que eso implica.
Es el caso de muchas start-ups que nacen de una idea, un producto mínimo viable (MVP)
y terminan convirtiéndose en un negocio rentable que hay que cuidar y hacer crecer.
Eso si, en cuanto las cosas empiecen a crecer mucho en algún momento habrá que
plantearse un cambio en la infraestructura y esto por supuesto, entre otras cosas, podría
incluir pasar a un framework más robusto.
¡Ok! ¿Pero porque debería implementar una arquitectura limpia como la arquitectura
hexagonal, que sin duda añade complejidad accidental, cuando uno de los puntos de fuerza
de Laravel es precisamente la sencillez y rapidez con la que nos permite desarrollar?
Con muy pocas lineas de código y abastecido de la suficiente cantidad de comida y bebida la
aplicación ya está hecha.
Además recuerda, que lo que no estás haciendo tú como desarrollador, lo está haciendo
Laravel detrás del telón, y esto está bién si desarrollas aplicaciones pequeñas, si pero
necesitas evolucionar tu producto, o tu infraestructura cambia, si necesitas más control
sobre tu código porque el proyecto está empezando a tener éxito y pasas de tener 4
peticiones (Tu, tu novia y tus padres) a tener 100.000 o más, puede que tu base de datos o
el mismo framework, o hasta la misma versión de framework ya no sean suficientes y
tengas que orientarte hacia otras soluciones que te proporcionen más robustez, nuevas
funcionalidades o más niveles de seguridad y fiabilidad.
Es en ese momento que entran en juego las arquitecturas limpias en todo su esplendor.
Estas, tienen el objetivo entre otros, de desacoplarnos de la infraestructura o, como en el
caso de Laravel cuyo ORM es Eloquent que utiliza el patrón active record, de reducir al
mínimo los llamados leaks de infraestructura a los cuales Laravel, por su naturaleza,
está más expuesto como veremos más adelante.
Un producto de mayor calidad al que se les pueden añadir mas fácilmente todas las
funcionalidades que nuestros clientes demandan (…bueno todas no, antes hay que
filtrar las que si y las que no).
Y ahora, después de esta extensa pero necesaria introducción, vamos a entrar más en
detalles.
Antes de pasar al código es importante definir algunos conceptos.
“Un bounded context es una parte bien definida de un software en la cual términos,
definiciones y reglas concretas pueden aplicarse de manera consistente”.
…ok… suena bien, pero puede que todavía no nos haya quedado muy claro, y entonces,
ya que a menudo se dice que un ejemplo vale más de mil palabras intentaré explicarlo
mejor con un ejemplo.
Imaginemos que nuestro proyecto prevé el desarrollo de una aplicación para los clientes
de una cadena de talleres de auto, y de un sistema de gestión para los empleados del
servicio de atención al cliente.
Es muy probable que, aunque en parte compartan según que terminología o concepto,
estos dos elementos tengan reglas de negocio que se aplican de manera diferente y por eso
la aplicación para clientes y el software de gestión del centro de atención pueden ser
identificados como dos bounded context diferentes, cada uno con su lógica.
Otro concepto del DDD alrededor del cual todo se mueve es el de dominio y este puede
resumirse como el campo de actividad en el cual se desarrolla nuestra lógica de negocio,
en él podemos encontrar los modelos de dominio que son la representación de las
entidades involucradas en los procesos.
En el caso de una entidad Cliente por ejemplo, sus value objects podrían ser el id, su
nombre, el email etc.
Ahora ya debería haber quedado más claro, si pero así no fuese, en internet puedes
encontrar toneladas de material al respecto que sin duda podrán ayudarte a entenderlo
mejor.
Arquitectura Hexagonal
La arquitectura hexagonal es un tipo de arquitectura limpia que se estructura en tres capas
o niveles de profundidad.
Podéis imaginárosla como círculos concéntricos uno dentro de otro cuyos elementos
tienen que respectar una regla muy sencilla: solo se pueden comunicar con otros
elementos que pertenecen a la misma capa o la siguiente más interna pero nunca con una
externa, es decir, desde fuera hacia dentro y nunca al revés.
La comunicación entre capas ocurre por medio de interfaces o puertos (la arquitectura
hexagonal también es llamada arquitectura puerto-adaptador, en inglés port-adapter).
Para una explicación más exhaustiva de este tipo de arquitectura os recomiendo leer un
articulo de Chris Fidao que lo explica muy bien. Hexagonal Architecture.
Representación de la Arquitectura Hexagonal
Infraestructura
Aquí podemos encontrar elementos como los controladores y las implementaciones de
los repositorios, todo aquello que representa un punto de contacto con nuestra
infraestructura y que potencialmente podría estar sujeto a los famosos leaks que
mencionaba al principio. Son los puntos de entrada y salida de nuestro flujo.
Aplicación
En esta capa solemos tener los casos de uso, también llamados acciones o servicios de
aplicación.
Los elementos de esta capa reciben en entrada el input que proviene de los elementos de
infraestructura como los controladores y se comunican con el dominio.
Dominio
Aquí es donde se encuentra la lógica de negocio, en esta capa vamos a encontrar
elementos como los modelos de dominio, sus value objects, servicios, eventos,
excepciones de dominio, las interfaces implementadas por los repositorios que se
encuentran en la capa de infraestructura etc.
Puede que en el momento en el que estás leyendo este articulo el código del repositorio haya
cambiado debido a la evolución del proyecto pero los conceptos siguen siendo los mismos,
osea, los de diseño guiado por dominio y de arquitectura hexagonal.
Podríamos enriquecerlos con validaciones utilizando el facade Validator que Laravel nos
proporciona pero no es el objeto de este articulo.
Al principio del archivo, en las declaraciones “use” podrás observar que el controlador solo se
está comunicando con la capa de aplicación e infraestructura.
CreateUserController en la carpeta Infrastructure
En el método __invoke del controlador extraemos los datos de la request que hemos
recibido, instanciamos el caso de uso CreateUserUseCase, le pasamos el repositorio por
constructor, luego llamamos el método __invoke del caso de uso y le pasamos los datos.
Observa las declaraciones “use”, el caso de uso solo se está comunicando con la capa de
dominio.
El método __invoke recibe los datos tipados de la request que han sido extraídos en el
controlador e instancia los value objects del modelo de dominio (aquí el tipo de dato que
se le pasa por constructor a cada value object es muy importante y tiene que coincidir con su
declaración).
Sucesivamente crea un nuevo usuario utilizando el named constructor create del modelo
de dominio User y lo persiste utilizando el método save del repositorio
EloquentUserRepository, el que hemos inyectado al instanciar el caso de uso y que
implementa la interfaz UserRepositoryContract.
CreateUserUseCase en la carpeta Application
Antes de pasar al método save del EloquentUserRepository vamos a mirar como está
estructurado un value object y el modelo de dominio User al que pertenece.
Cuando instanciamos un value object desde el caso de uso le pasamos por constructor el
dato correspondiente (un email en este caso) del tipo correspondiente, en el caso de este
value object en concreto, espera recibir un dato de tipo string y lo primero que hace una
vez se haya instanciado el objeto lo valida por medio del método privado validate,
sucesivamente lo inicializa para que podamos acceder a el desde el exterior a través del
método value.
UserEmail value object en la capa de dominio
Al modelo de dominio User se le pasan por constructor todas las propriedades que
lo definen, esto nos obliga a que cada vez que vamos a crear un nuevo usuario el modelo
disponga de todos los datos necesarios para poder guardarlo.
Se suele utilizar este enfoque para garantizar integridad de datos en nuestro sistema de
persistencia y evitar encontrarnos con entidades en nuestra base de datos que tengan datos
parciales y que sean por ende inconsistentes.
Modelo de dominio User en la capa de dominio
El modelo también dispone de unos métodos (getters) que nos permiten acceder a sus
propriedades, de un named constructor (el método create) que a partir de los datos que
recibe instancia el modelo, y podría contener otros métodos que definen su
comportamiento, esto porque la idea a la base de este tipo de enfoque es tener modelos
ricos en comportamiento y poder aplicar el principio del “Tell don’t Ask” planteado por
Martin Fowler.
Ahora si podemos pasar a ver lo que hace el método save una vez reciba el nuevo usuario
que ha sido creado.
Eloquent está basado en el patrón active record, y esto representa la mayor diferencia
con respecto a un framework como Symfony, cuyo ORM es Doctrine que por lo contrario
utiliza el patrón data mapper.
Esto nos permite tener acceso a todas las propriedades y métodos del modelo User del
Laravel y aprovechar los métodos que Eloquent nos proporciona.
En definitiva lo que hacemos es crear una capa de abstracción que nos permite mantener
nuestra arquitectura a salvo de posibles contaminaciones y al mismo tiempo aprovechar
las funcionalidades que el framework y su ORM nos brindan.
Repositorio EloquentUserRepository en la capa de infraestructura
El método save, lo que hace es recibir el modelo de dominio User como parámetro y
mapear sus atributos en un array que se le pasa al método create del EloquentUserModel
para que lo persista.
Una vez haya sido guardado el nuevo usuario, el controlador de nuestra arquitectura lo
recupera utilizando el caso de uso GetUserByCriteriaUseCase y lo devuelve al
controlador de Laravel que lo envía al solicitante de la petición.
Conclusiones
Aunque DDD y Arquitectura Hexagonal parezcan temas complejos al principio, y sin duda
lo son a nivel teórico, a nivel practico su implementación no se traduce en absoluto en
miles de líneas de código, todo lo contrario.
De hecho nuestros métodos son mucho más atómicos, las responsabilidades están bien
definidas, el software es más robusto, fiable, escalable, y detalle a tomar en cuenta,
reutilizable.
Agradecimientos
Espero que el contenido de este articulo pueda ayudar a quien como yo sigue buscando
soluciones intentando mejorar día tras día y noche tras noche.
Todo está en proceso y las cosas siempre se pueden mejorar pero por algo se empieza y
cualquier feedback será bienvenido.
Quiero agradecer a Rafael Gomez y Javier Ferrer de CodelyTV por compartir a través de
su canal en Youtube sus conocimientos y experiencia con toda la comunidad de
desarrolladores de una manera muy clara y divertida.