¿Cómo exactamente se debe validar y transformar un comando CQRS en un objeto de dominio?

22

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 Carusa dos componentes:

  • Transmission,
  • Engine.

Tanto Transmissiony Engineobjetos de valor están representadas como Super tipos y tienen según sub tipos, Automaticy Manuallas transmisiones, o Petroly Electricmotores respectivamente.

En este dominio, vivir por sí solo es un bien creado Transmission, sea Automatico no Manual, o cualquier tipo de Engine. Pero el Caragregado introduce algunas reglas nuevas, aplicables solo cuando Transmissiony los Engineobjetos se usan en el mismo contexto. A saber:

  • Cuando un automóvil usa Electricmotor, el único tipo de transmisión permitido es Automatic.
  • Cuando un automóvil usa Petrolmotor, puede tener cualquier tipo de Transmission.

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 CreateCarcomando 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 CreateUsercomando.

El comando contiene un Idusuario 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 UserRepositorycomo 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 Useragregado y que el validador de comandos solo debe verificar la unicidad y, si la validación tiene éxito, debo proceder a crear el Useragregado en el agregado CreateUserCommandHandlery 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 UserRepositorycon un Userobjeto 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.

Andy
fuente
Necesito algún código de ejemplo, creo. ¿Cómo son sus objetos de comando y dónde los crea?
Ewan
@Ewan Agregaré ejemplos de código más tarde hoy o mañana. Partiendo para un viaje en unos minutos.
Andy
Como programador de PHP, sugiero echar un vistazo a mi implementación de CQRS + ES: github.com/xprt64/cqrs-es
Constantin Galbenu
@ConstantinGALBENU Si consideramos que la interpretación de Greg Young de CQRS es correcta (lo que probablemente deberíamos hacer), entonces su comprensión de CQRS es incorrecta, o al menos su implementación de PHP lo es. Los comandos no deben ser manejados por agregados directamente. Los comandos deben ser manejados por controladores de comandos que pueden producir cambios en los agregados que luego producen eventos para ser utilizados para las replicaciones de estado.
Andy
No creo que nuestras interpretaciones sean diferentes. Solo tiene que cavar más en DDD (en el nivel táctico de los agregados) o abrir más los ojos. Existen al menos dos estilos de implementación de CQRS. Yo uso uno de ellos. Mi implementación se parece más al modelo de actor y hace que la capa de aplicación sea muy delgada, lo que siempre es algo bueno. Observé que hay una gran cantidad de duplicación de código dentro de esos servicios de aplicación y decidí reemplazarlos con a CommandDispatcher.
Constantin Galbenu

Respuestas:

22

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).

Entonces, la verdadera pregunta es: ¿Dónde está exactamente la lógica?

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:

  • la estructura del objeto de comando en sí mismo; el constructor del comando tiene algunos campos obligatorios que deben estar presentes para que se cree el comando; Esta es la primera y más rápida validación; esto obviamente está contenido en el comando.
  • validación de campo de bajo nivel, como el no vacío de algunos campos (como el nombre de usuario) o el formato (una dirección de correo electrónico válida). Este tipo de validación debe estar contenido dentro del comando en sí, en el constructor. Hay otro estilo de tener un isValidmé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 validatorsclases 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 validatorspodrí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.
  • Los métodos agregados que reciben y procesan los comandos. Esta es la última (tipo de) validación que ocurre. El agregado extrae los datos del comando y utiliza cierta lógica empresarial central que acepta (realiza cambios en su estado) o lo rechaza. Esta lógica se verifica de una manera fuerte y consistente. Esta es la última línea de defensa. En su ejemplo, la regla When a car uses Electric engine the only allowed transmission type is Automaticdebe verificarse aquí.

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 de guardar en el UserRepository con un objeto User directamente que podría conducir a un error fatal de la base de datos, porque el correo electrónico podría haber sido demasiado tiempo

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 CommandDispatcherpara 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.

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.

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 en HTTP 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:

La ruta del comando a través de validadores de comando en su camino hacia el Agregado

Constantin Galbenu
fuente
Con respecto a su estrategia de validación, el punto número dos me llama la atención como un lugar probable donde la lógica se duplicará con frecuencia. Ciertamente, uno querría que el agregado del Usuario valide un correo electrónico no vacío y bien formado, ¿no? Esto se hace evidente cuando presentamos un comando ChangeEmail.
King-side-slide
@ king-side-slide no si tienes un EmailAddressobjeto de valor que se valide a sí mismo.
Constantin Galbenu
Eso es completamente correcto. Uno podría encapsular un EmailAddresspara 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. EmailAddressEs un ejemplo conveniente porque toda la concepción de este valor tiene requisitos de validación global.
King-side-slide
Del mismo modo, la idea de un "validador de comandos" parece innecesaria. El objetivo no es evitar que se creen y envíen comandos no válidos. El objetivo es evitar que se ejecuten. Por ejemplo, puedo pasar cualquier información que quiera con una URL. Si no es válido, el sistema rechaza mi solicitud. El comando aún se crea y se envía. Si un comando requiere múltiples agregados para la validación (es decir, una colección de Usuarios para verificar la unicidad del correo electrónico), un servicio de dominio es mejor. Los objetos como "x validator" suelen ser un signo de un modelo anémico en el que los datos se separan del comportamiento.
diapositiva lateral del rey
1
@ king-side-slide Un ejemplo concreto es 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.
Constantin Galbenu
6

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 ChangeEmailcomando, 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:

  • el correo electrónico no puede estar vacío
  • el correo electrónico no puede tener más de 100 caracteres
  • el correo electrónico debe ser único

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 Useragregado, 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 a User, pero realmente no. La "singularidad" de un correo electrónico se aplica a una colección de Users(según algún alcance).

Ah ja! Con eso en mente, queda muy claro que su UserRepository(su colección en memoria Users) 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 una UserEmailAlreadyExistsexcepción). Alternativamente, un dominio UserServicepodría hacerse responsable de crear nuevos Usersy 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.

tobogán lateral
fuente
2
Estoy de acuerdo con ésto. Mi lectura hasta ahora (sin CQRS) me dice que la validación siempre debe ir en el modelo de dominio para proteger a los invariantes. Ahora estoy leyendo CQRS, me está diciendo que ponga la validación en los objetos Command. Esto parece contrario a la intuición. ¿Conoces algún ejemplo, por ejemplo, en GitHub donde la validación se coloca en el modelo de dominio en lugar del comando? +1.
w0051977