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
.Respuestas:
La siguiente respuesta está en el contexto del estilo CQRS promovido por cqrs.nu en el que los comandos llegan directamente a los agregados. En este estilo arquitectónico, los servicios de la aplicación están siendo reemplazados por un componente de infraestructura ( CommandDispatcher ) que identifica el agregado, lo carga, le envía el comando y luego persiste el agregado (como una serie de eventos si se usa el abastecimiento de eventos).
Hay múltiples tipos de lógica (validación). La idea general es ejecutar la lógica lo antes posible: falle rápidamente si lo desea. Entonces, las situaciones son las siguientes:
isValid
método, pero esto me parece inútil, ya que alguien tendría que recordar llamar a este método cuando, de hecho, una instanciación de comando exitosa debería ser suficiente.command validators
clases separadas que tienen la responsabilidad de validar un comando. Uso este tipo de validación cuando necesito verificar la información de múltiples agregados o fuentes externas. Puede usar esto para verificar la unicidad de un nombre de usuario.Command validators
podría tener cualquier dependencia inyectada, como repositorios. ¡Tenga en cuenta que esta validación finalmente es coherente con el agregado (es decir, cuando se crea el usuario, se puede crear otro usuario con el mismo nombre de usuario mientras tanto)! Además, ¡no intente poner aquí la lógica que debería residir dentro del agregado! Los validadores de comandos son diferentes de los administradores de Sagas / Process que generan comandos basados en eventos.When a car uses Electric engine the only allowed transmission type is Automatic
debe verificarse aquí.Usando las técnicas anteriores, nadie puede crear comandos inválidos o pasar por alto la lógica dentro de los agregados. Los validadores de comandos se cargan automáticamente + son llamados por el
CommandDispatcher
para que nadie pueda enviar un comando directamente al agregado. Uno podría llamar a un método en el agregado pasando un comando, pero no podría persistir los cambios, por lo que sería inútil / inofensivo hacerlo.También soy un programador de PHP y no devuelvo nada de mis controladores de comandos (métodos agregados en el formulario
handleSomeCommand
). Sin embargo, con bastante frecuencia, devuelvo información al cliente / navegador enHTTP response
, por ejemplo, la ID de la raíz agregada recién creada o algo de un modelo de lectura, pero nunca devuelvo (realmente nunca ) nada de mis métodos de comando agregado. El simple hecho de que el comando fue aceptado (y procesado, estamos hablando de procesamiento PHP sincrónico, ¿verdad?) Es suficiente.Devolvemos algo al navegador (y seguimos haciendo CQRS según el libro) porque CQRS no es una arquitectura de alto nivel .
Un ejemplo de cómo funcionan los validadores de comandos:
fuente
EmailAddress
objeto de valor que se valide a sí mismo.EmailAddress
para reducir la duplicación. Sin embargo, lo que es más importante, al hacerlo, también estaría moviendo la lógica de su comando a su dominio. Vale la pena señalar que esto puede llevarse demasiado lejos. A menudo, los conocimientos similares (objetos de valor) pueden tener diferentes requisitos de validación según quién los use.EmailAddress
Es un ejemplo conveniente porque toda la concepción de este valor tiene requisitos de validación global.UserCanPlaceOrdersOnlyIfHeIsNotLockedValidator
. Puede ver que este es un dominio separado del de los pedidos, por lo que no puede ser validado por el agregado de pedidos.Una premisa fundamental de DDD es que los modelos de dominio se validan a sí mismos. Este es un concepto crítico porque eleva su dominio como la parte responsable de asegurarse de que se cumplan las reglas de su negocio. También mantiene su modelo de dominio como foco para el desarrollo.
Un sistema CQRS (como usted señala correctamente) es un detalle de implementación que representa un subdominio genérico que implementa su propio mecanismo cohesivo. Su modelo no debe depender en modo alguno de ninguna pieza de infraestructura CQRS para comportarse de acuerdo con las reglas de su negocio. El objetivo de DDD es modelar el comportamiento de un sistema de manera que el resultado sea una abstracción útil de los requisitos funcionales de su dominio comercial principal. Mover cualquier parte de este comportamiento fuera de su modelo, por tentador que sea, reduce la integridad y la cohesión de su modelo (y lo hace menos útil).
Simplemente extendiendo su ejemplo para incluir un
ChangeEmail
comando, podemos ilustrar perfectamente por qué no desea que su lógica de negocios esté en su infraestructura de comandos, ya que necesitaría duplicar sus reglas:Entonces, ahora que podemos estar seguros de que nuestra lógica debe estar en nuestro dominio, abordemos el tema de "dónde". Las primeras dos reglas se pueden aplicar fácilmente a nuestro
User
agregado, pero esa última regla es un poco más matizada; uno que requiere un poco más de conocimiento para obtener una visión más profunda. En la superficie, puede parecer que esta regla se aplica a aUser
, pero realmente no. La "singularidad" de un correo electrónico se aplica a una colección deUsers
(según algún alcance).Ah ja! Con eso en mente, queda muy claro que su
UserRepository
(su colección en memoriaUsers
) puede ser un mejor candidato para hacer cumplir esta invariante. El método "guardar" es probablemente el lugar más razonable para incluir el cheque (donde puede lanzar unaUserEmailAlreadyExists
excepción). Alternativamente, un dominioUserService
podría hacerse responsable de crear nuevosUsers
y actualizar sus atributos.Fail fast es un buen enfoque, pero solo se puede hacer donde y cuando encaja con el resto del modelo. Puede ser extremadamente tentador verificar los parámetros en un método de servicio de aplicación (o comando) antes de seguir procesando en un intento de detectar fallas cuando usted (el desarrollador) sabe que la llamada fallará en algún lugar más profundo del proceso. Pero al hacerlo, habrá duplicado (y filtrado) conocimiento de una manera que probablemente requerirá más de una actualización del código cuando cambien las reglas del negocio.
fuente