Llevo bastante tiempo adaptando el CQRS 1 de los pobres porque me encanta su flexibilidad para tener datos granulares en un almacén de datos, proporcionando grandes posibilidades de análisis y, por lo tanto, aumentando el valor comercial y, cuando sea necesario, otro para lecturas que contienen datos desnormalizados para un mayor rendimiento .
Pero desafortunadamente desde el principio he estado luchando con el problema en el que exactamente debería colocar la lógica de negocios en este tipo de arquitectura.
Por lo que entiendo, un comando es un medio para comunicar la intención y no tiene vínculos con un dominio por sí mismo. Son básicamente objetos de transferencia de datos (tontos, si lo desea). Esto es para hacer que los comandos sean fácilmente transferibles entre diferentes tecnologías. Lo mismo se aplica a los eventos como respuestas a eventos completados con éxito.
En una aplicación DDD típica, la lógica de negocios reside en entidades, objetos de valor, raíces agregadas, son ricos tanto en datos como en comportamiento. Pero un comando no es un objeto de dominio, por lo tanto, no debe limitarse a las representaciones de datos de dominio, porque eso les exige demasiado.
Entonces, la verdadera pregunta es: ¿Dónde está exactamente la lógica?
He descubierto que tiendo a enfrentar esta lucha con mayor frecuencia cuando intento construir un agregado bastante complicado que establece algunas reglas sobre combinaciones de sus valores. Además, cuando modelo objetos de dominio, me gusta seguir el paradigma de falla rápida , sabiendo que cuando un objeto alcanza un método está en un estado válido.
Digamos que un agregado Car
usa dos componentes:
Transmission
,Engine
.
Tanto Transmission
y Engine
objetos de valor están representadas como Super tipos y tienen según sub tipos, Automatic
y Manual
las transmisiones, o Petrol
y Electric
motores respectivamente.
En este dominio, vivir por sí solo es un bien creado Transmission
, sea Automatic
o no Manual
, o cualquier tipo de Engine
. Pero el Car
agregado introduce algunas reglas nuevas, aplicables solo cuando Transmission
y los Engine
objetos se usan en el mismo contexto. A saber:
- Cuando un automóvil usa
Electric
motor, el único tipo de transmisión permitido esAutomatic
. - Cuando un automóvil usa
Petrol
motor, puede tener cualquier tipo deTransmission
.
Podría captar esta infracción de combinación de componentes al nivel de crear un comando, pero, como he dicho antes, por lo que entiendo, no debería hacerse porque el comando contendría entonces lógica empresarial que debería limitarse a la capa de dominio.
Una de las opciones es mover esta validación de lógica de negocios al validador de comandos en sí, pero esto tampoco parece ser correcto. Parece que estaría deconstruyendo el comando, verificando sus propiedades recuperadas usando getters y comparándolos dentro del validador e inspeccionando los resultados. Eso me grita como una violación de la ley de Deméter .
Descartando la opción de validación mencionada porque no parece viable, parece que uno debería usar el comando y construir el agregado a partir de él. Pero, ¿dónde debería existir esta lógica? ¿Debería estar dentro del controlador de comando responsable de manejar un comando concreto? ¿O tal vez debería estar dentro del validador de comandos (tampoco me gusta este enfoque)?
Actualmente estoy usando un comando y creo un agregado a partir de él dentro del controlador de comando responsable. Pero cuando hago esto, si tuviera un validador de comandos, no contendría nada, porque si el CreateCar
comando existiera, entonces contendría componentes que sé que son válidos en casos separados, pero el agregado podría decir diferente.
Imaginemos un escenario diferente que mezcla diferentes procesos de validación: crear un nuevo usuario con un CreateUser
comando.
El comando contiene un Id
usuario que se habrá creado y su Email
.
El sistema establece las siguientes reglas para la dirección de correo electrónico del usuario:
- debe ser único,
- no debe estar vacío
- debe tener como máximo 100 caracteres (longitud máxima de una columna de base de datos).
En este caso, aunque tener un correo electrónico único es una regla comercial, verificarlo en conjunto no tiene mucho sentido, porque necesitaría cargar todo el conjunto de correos electrónicos actuales en el sistema en una memoria y verificar el correo electrónico en el comando contra el agregado ( ¡Eeeek! Algo, algo, rendimiento). Debido a eso, movería esta verificación al validador de comandos, que tomaría UserRepository
como una dependencia y usaría el repositorio para verificar si ya existe un usuario con el correo electrónico presente en el comando.
Cuando se trata de esto, de repente tiene sentido poner las otras dos reglas de correo electrónico también en el validador de comandos. Pero tengo la sensación de que las reglas deben estar realmente presentes dentro de un User
agregado y que el validador de comandos solo debe verificar la unicidad y, si la validación tiene éxito, debo proceder a crear el User
agregado en el agregado CreateUserCommandHandler
y pasarlo a un repositorio para guardarlo.
Me siento así porque es probable que el método de guardar del repositorio acepte un agregado que garantiza que una vez que se pasa el agregado, se cumplen todos los invariantes. Cuando la lógica (por ejemplo, el no vacío) solo está presente dentro de la validación del comando en sí, otro programador podría omitir por completo esta validación y llamar al método guardar directamente UserRepository
con un User
objeto que podría conducir a un error fatal de la base de datos, porque el correo electrónico podría haber pasado demasiado tiempo.
¿Cómo manejas personalmente estas complejas validaciones y transformaciones? Estoy contento con mi solución, pero siento que necesito una afirmación de que mis ideas y enfoques no son completamente estúpidos para estar bastante contento con las opciones. Estoy completamente abierto a enfoques completamente diferentes. Si tiene algo que ha probado personalmente y funcionó muy bien para usted, me encantaría ver su solución.
1 Al trabajar como desarrollador de PHP responsable de crear sistemas RESTful, mi interpretación de CQRS se desvía un poco del enfoque estándar de procesamiento de comandos asíncronos , como a veces devolver resultados de comandos debido a la necesidad de procesar comandos sincrónicamente.
CommandDispatcher
.