En el año 1994, desarrollar páginas web era algo bastante complicado. Casi todo el mundo usaba CGI que se programa en C o PERL, y resultaba muy engorroso. Por aquella época Rasmus, ideo una serie de herramientas para hacer páginas webs personales, y lo llamó PHP (Personal Home Page Tools.)
En los años sucesivos empresas como Microsoft, Oracle o Sun, apostaron fuerte por desarrollar y adaptar sus lenguajes hacia el desarrollo web, teniendo una gran acogida en el mundo corporativo.
Mientras tanto, PHP evolucionó de herramienta a lenguaje. Era tan sencillo que se usó por aficionados, novatos, y sobre todo proyectos de open source. Lo que permitió un boom nunca antes visto en eCommerce, Blogging, Prensa digital, etc.. Pero aplicaciones como osCommerce, o las primeras versiones de WordPress, PrestaShop, etc.. eran un desastre total desde el punto de vista de programación, pero funcionaban.
Pero las empresas, debido a que era más fácil contratar programadores en PHP, fueron adoptándolo dentro de sus grandes proyectos, provocando principalmente dos cosas:
- Una gran deuda técnica en miles de empresas
- Y que una pequeña parte de los programadores apostaran por mejorar, por crecer, por hacer las cosas bien.
El status actual de los lenguajes de programación, es que casi todos han alcanzado un nivel de madurez suficiente que permiten hacer casi cualquier tipo de desarrollo (incluido PHP), pero queda una larga lista de proyectos Legacy que aún están en producción y que necesitan evolucionar.
DDD y Arquitectura Hexagonal
Durante muchas décadas, la arquitectura preferida en el mundo empresarial había sido la Arquitectura por Capas. En esta arquitectura principalmente teníamos:
- Application Layer. La capa que interactúa con el usuario.
- Service/Business Layer. Donde reside la lógica de negocios.
- Data Access Layer. El acceso a Base de Datos
A medida que se iban haciendo proyectos más complejos y más grandes, se echaba en falta arquitecturas orientadas en el negocio, y no en como resolverlo. Es decir, el “qué” era más importante que el “cómo”. Para ello nació DDD (Domain Driven Design), no sólo para poder trasladar las necesidades de negocio al código, sino para que el código y el negocio se parecieran lo máximo posible. La idea general es que alguien de negocio, casi podía entender el código, o al menos la estructura de clases.
Yo me encontré con DDD la primera vez en 2006 y aunque entendía los motivos, no fui capaz de llevarlo a cabo. Y esta era la queja general de muchos programadores que vieron DDD como un utopía.
En paralelo a DDD, nació también la Arquitectura Hexagonal, enfocada al “cómo”, pero olvidando el “qué”. Fue tomando adeptos, pero no llegaba a despegar por que no escalaba bien cuando el negocio era complejo.
Hasta que en un momento dado, se empieza a aplicar DDD sobre Arquitectura Hexagonal y todo empieza a cobrar sentido. Y es el momento en que se empieza adoptar. Pero:
- Los grandes proyectos empresariales seguían desarrollándose en arquitectura de capas, adaptadas a las nuevas necesidades, pero no dejaba de ser esa arquitectura probada que todo el mundo conocía.
- PHP, Node.js, etc… que no habían pasado por esa etapa y que sabían que necesitaban algo, saltaron a Arquitectura Hexagonal y/o DDD de una u otra forma.
Hasta que de todo se empieza a desmoronar. El problema de aplicar DDD, es que para hacerlo, el equipo tiene que entender los motivos, tiene que entender el por qué de cada una de las decisiones que se deben tomar.
- ¿Por qué un comando no puede devolver valores?
- ¿Por qué usar Foreign Keys no me permite escalar?
- ¿Por qué usar UUID/UILD en lugar de incrementales?
Son preguntas que la mayoría de la gente que conozco que programa en DDD no sabe responder, sabe lo que tiene que hacer, pero no el motivo.
Pero claro, la formación que reciben, los ejemplos que ven, no lo explican todo, y antes el primer problema dónde tienen que pensar como hacerlo, pues la lían y bastante.
Os recomiendo esta entrevista a programadores de Pc Componentes, que después de hacer los cursos de CodelyTV, pasaron una parte importante a DDD, y en el primer BlackFriday… boom, pinchazo. Acabaron cambiado de CTO y alejándose del DDD “puro”.
El segundo problema que tiene DDD es el rendimiento, si aplicas DDD puro, tendrás una tortuga. Los puristas de DDD suelen decir que es lento pero que escala, así pasé yo de 2 servidores a 20. Aprendí la lección rápido.
Ya contaré en otros artículos por qué es lento y como solucionarlo sin saltarnos mucho la teoría.
¿Y que hago con mi proyecto Legacy?
Imagina que tienes un proyecto en PHP, que funciona, que factura, pero:
- Es complicado de mantener, cuando tienes que tocar algo, encontrar el código es complejo. La misma funcionalidad está repartida en montón de controladores, servicios, etc…
- Cuando hay que hacer algo nuevo, cada programador lo hace de una forma diferente, y probablemente repita el código por que no encuentra si alguien lo ha hecho antes, o es incapaz de reutilizarlo.
- Es complicado hacer tests, por que no puedes probar las cosas fácilmente, todo tiene demasiadas dependencias.
- Si contratas a alguien, formarle para que sea productivo es complicado, por que “no hay una forma de trabajar standard”.
He estado en esta situación muchas veces, y he compartido experiencias simulares con otros DTOs. Ante este problema puedes optar por:
1.- Lo haces de nuevo. He participado en esto en empresas como Endesa, El Corte Inglés o Privalia. Y nunca se acaba, es un camino condenado al fracaso. Mas aún, en ningún caso, (independientemente del número de programadores), en el primer año, no llega a pasar el del 10% de los requisitos.
2.- Aplicas DDD + Arquitectura Hexagonal. Que salvo que tengas programadores muy, muy, muy buenos, que entiendan los motivos, es un fracaso asegurado también.
3.- Buscar algo intermedio, sencillo de usar y con años y años de rodaje. Como la arquitectura por capas (que lo mismo lo estás usando si saberlo).
Aplicando Arquitectura por Capas de forma fácil en PHP
Vamos a dividir nuestro código en 3 capas; Aplicación, Servicios e Infraestructura (acceso a base de datos u otros servicios externos)
Capa de Aplicación
En esta capa meteremos todo lo relacionado con exponer nuestro negocio al usuario y otras aplicaciones: controladores, API, vistas, etc..
Intentaremos aplicar al máximo posible SOLID, pero como la gente suele confundir casi todas las letras, yo me contento con a la S. Principio de responsabilidad única.
Para hacer los controladores, aplicaremos el patrón Front Controller. Es decir, el controlador sólo se dedica a controlar interacción de peticiones de usuario con el negocio. Sólo eso. Se acabó controladores de miles de lineas.
Ejemplo:
final class CustomerController
{
public function addCustomer(
AddCustomerRequest $request,
AddCustomerAction $action): void
{
$action($request->getCustomerData());
}
}
Por cada endpoint tenemos:
- Una clase encargada de recoger los datos (Request), estos datos se devuelven como un Dto. Aquí podemos poner validaciones formales de datos. Es decir, el nombre es un string, la edad en un entero, etc…
- Una clase encargada de realizar la acción (Action). Que recibe el Dto y llama a nuestra capa de servicio para realizar la acción.
- Un método en el controlador, que sólo puede hacer:
- Llamar al action.
- Devolver datos.
- Redirigir a otra url.
De esta forma, desarrollar endpoints es super fácil, el código está separado, y lo mejor de todo: Hacer un test del Action es pan comido, por que no está acoplado a nada.
Capa de servicios
Antes comenté que la arquitectura por capas había ido evolucionando para acercarce más al negocio. Una de las soluciones, es mapear cada necesidad de negocio en un “Caso de Uso”. Algo fácilmente entendible por gente de negocio y programadores.
Para nuestro ejemplo, agregar un cliente quedaría así:
final class AddCustomerUseCase
{
public function __invoke(string $name, string $email): void
{
$customer = Customer::createNew(name: $name, email: $email);
$this->repository->store($customer);
}
}
Es importante que la mayoría de los casos de uso sólo hagan una cosa, que tengan una sola responsabilidad. Habrá casos en los que tendremos servicios complejos que llamarán a varios casos de uso.
Capa de Acceso a Datos
Esta es una parte que puede ser sencilla o compleja dependiendo si queremos alejarnos o no del patrón Active Record.
Veamos al ejemplo anterior:
$customer = Customer::createNew(name: $name, email: $email);
¿Por qué no lo hacemos usando new Customer y estableciendo el valor de las propiedades?
Muy sencillo, ¿cómo nos aseguramos que al crear un cliente el registro sea consistente y no se nos olvide el email o cualquier propiedad? Al usar el patrón “named constructor”, nos aseguramos que los registros que creamos son correctos.
¿Y por que no usar un validate()?
Podemos usar validate, pero daría el error en tiempo de ejecución, mientras que con named constructor, el código no se puede ejecutar, se detecta antes con herramientas como PHPStan.
Ahora toca guardar el registro:
$this->repository->store($customer);
Si te fijas, podría haber hecho $customer->save() que parece más sencillo. Pero de esta forma podemos centralizar en un sólo sitio donde se almacenan los clientes, evitamos que se llame desde la capa de aplicación, y es mucho más fácil de probar el código. Podemos mockear el método del repositorio store, es más complicado mokear el save del model Active Record.
Accediendo a Datos
Un ejemplo sencillo. Necesito un listado de pedidos enviados de un cliente, y tradicionalmente lo habíamos resuelto como:
$sentOrders = Orders::query()
->where('status', OrderStatusEnum::sent)
->where('customerId', $customerId)->get()->all();
Este código siempre acaba repetido en varios casos de uso, para la pantalla de ver pedidos anteriores, para seleccionar un pedido a devolver, para la pantalla de atención al cliente, etc….
Imagina ahora que agregamos un estado nuevo “entregado”. Todas estas queries dejarían de funcionar.
Aquí entra en juego el repositorio:
public function getSentOrders(bool $includeDelivered = true): array
{
......
Con esto nos aseguramos que la lógica de negocios para devolver los pedidos siempre es la misma.
Ojo que esta forma de trabajar tiene una desventaja, podemos acabar con un repositorio lleno de funciones, por que cada vez que un programador necesita una query, en lugar de usar una que ya existe se crea una nueva, y el remedio es peor que la enfermedad.
Como consejo general:
- Para queries complejas, queries con un significado de negocio importante, obligaría a usar el repositorio.
- Para queries sencillas (como devolver por ID), o queries que sabemos que no se va a usar en ningún otro sitios, como Queries para informes o estadísticas. Me saltaría el repositorio.
Testing
Si aplicas las 3 capas que hemos comentado, es muy fácil hacer pruebas de integración completas, o pruebas de cada uno de los componentes de forma separada.
Inicialmente, yo me centraría en:
- Pruebas de integración críticas. Por ejemplo, en un eCommerce, comprar y registro.
- Pruebas de casos de uso complejos o con muchos cálculos. Cómo el cálculo de IVA de un pedido.
Es importante entender cómo funciona el Mockeo de objetos (por ejemplo con Mockery), necesario para poder hacer tests de capas de forma indendiente.
Uniéndolo todo
Ahora que más o menos tenemos todo claro ¿por dónde empiezo?. Depende de tu negocio, de tus necesidades, de tu equipo, pero en lineas generales:
- Documenta cómo vais a trabajar. Que todos los programadores tengan una referencia que puedan leer. Mantén esta documentación actualizada. Seguro que surgen nuevas necesidades.
- Aplica todo lo explicado en una parte acotada, no te pongas a cambiarlo todo:
- Al aplicar esta arquitectura seguro que cometerás algún que otro fallo, acabaras rehaciendo el código varias veces hasta que lo tengas claro.
- Una vez que estés satisfecho con el resultado, aplicar al resto.
- Cuando decidas por donde empezar, intenta hacerlo completo. Es decir, si quieres empezar con Customer, no dejes la mitad del código de una forma y la otra mitad con Capas. Si tienes que hacerlo por falta de tiempo, recursos o complejidad, marca como @deprecated todo el código legacy.
- Cada vez que tengáis que tocar un código marcado como @deprecated, tenéis que evaluar si merece la pena seguir aumentando la deuda o mejor hacerlo de nuevo.
Y creo que esto es suficiente para empezar, podría extenderme más, aplicar más reglas pero acabaría llegando a DDD, que no es el objetivo.
Por cierto, mucha gente que dice que hace DDD, en realidad hace lo que he explicado aquí.
Leave a Reply