design-patterns - microsoft - materialized view pattern
CQRS: Valores de retorno del comando (4)
Parece haber una confusión infinita sobre si los comandos deberían o no tener valores de retorno. Me gustaría saber si la confusión es simplemente porque los participantes no han declarado su contexto o circunstancias.
La confusión
Aquí hay ejemplos de la confusión ...
-
Udi Dahan dice que los comandos "no devuelven errores al cliente", pero en el mismo artículo muestra un diagrama donde los comandos de hecho devuelven errores al cliente.
-
Un artículo de Microsoft Press Store dice "el comando ... no devuelve una respuesta", pero luego da una advertencia ambigua:
A medida que la experiencia en el campo de batalla crece alrededor de CQRS, algunas prácticas se consolidan y tienden a convertirse en mejores prácticas. En parte contrario a lo que acabamos de decir ... hoy es una opinión común pensar que tanto el controlador de comandos como la aplicación necesitan saber cómo fue la operación transaccional. Los resultados deben ser conocidos ...
- Jimmy Bogard dice que "los comandos siempre tienen un resultado ", pero luego hace un esfuerzo adicional para mostrar cómo los comandos vuelven nulos.
Bueno, ¿los controladores de comando devuelven valores o no?
¿La respuesta?
Siguiendo el ejemplo de los " Mitos CQRS " de Jimmy Bogard, creo que la (s) respuesta (s) a esta pregunta depende de qué "cuadrante" programático / contextual del que esté hablando:
+-------------+-------------------------+-----------------+
| | Real-time, Synchronous | Queued, Async |
+-------------+-------------------------+-----------------+
| Acceptance | Exception/return-value* | <see below> |
| Fulfillment | return-value | n/a |
+-------------+-------------------------+-----------------+
Aceptación (por ejemplo, validación)
El comando "Aceptación" se refiere principalmente a la validación. Presumiblemente, los resultados de la validación deben entregarse sincrónicamente a la persona que llama, ya sea que el comando "cumplimiento" sea síncrono o en cola.
Sin embargo, parece que muchos profesionales no inician la validación desde el controlador de comandos. Por lo que he visto, es porque (1) ya han encontrado una forma fantástica de manejar la validación en la capa de aplicación (es decir, un controlador ASP.NET MVC que verifica el estado válido a través de anotaciones de datos) o (2) una arquitectura está en su lugar, lo que supone que los comandos se envían a un bus o cola (fuera de proceso). Estas últimas formas de asincronía generalmente no ofrecen semánticas o interfaces de validación síncrona.
En resumen, muchos diseñadores pueden querer que el controlador de comandos proporcione resultados de validación como un valor de retorno (síncrono), pero deben vivir con las restricciones de las herramientas de asincronía que están utilizando.
Cumplimiento
Con respecto al "cumplimiento" de un comando, el cliente que emitió el comando podría necesitar conocer la identidad del alcance para un registro recién creado o tal vez información de falla, como "sobregiro de cuenta".
En una configuración en tiempo real, parece que un valor de retorno tiene más sentido; Las excepciones no deben utilizarse para comunicar los resultados de fallas relacionadas con el negocio. Sin embargo, en un contexto de "colas" ... los valores de retorno naturalmente no tienen sentido.
Aquí es donde se puede resumir toda la confusión:
Muchos (¿la mayoría?) Los profesionales de CQRS asumen que ahora, o en el futuro, incorporarán un marco o plataforma de asincronía (un bus o una cola) y, por lo tanto, proclaman que los manejadores de comandos no tienen valores de retorno. Sin embargo, algunos profesionales no tienen la intención de usar tales construcciones controladas por eventos, por lo que respaldarán los controladores de comandos que (sincrónicamente) devuelven valores.
Entonces, por ejemplo, creo que se asumió un contexto síncrono (solicitud-respuesta) cuando Jimmy Bogard proporcionó esta interfaz de comando de muestra :
public interface ICommand<out TResult> { }
public interface ICommandHandler<in TCommand, out TResult>
where TCommand : ICommand<TResult>
{
TResult Handle(TCommand command);
}
Su producto Mediatr es, después de todo, una herramienta en memoria. Dado todo esto, creo que la razón por la que Jimmy se tomó el tiempo para producir un retorno nulo de un comando no fue porque "los manejadores de comandos no deberían tener valores de retorno", sino porque simplemente quería que su clase Mediator tuviera una interfaz consistente:
public interface IMediator
{
TResponse Request<TResponse>(IQuery<TResponse> query);
TResult Send<TResult>(ICommand<TResult> query); //This is the signature in question.
}
... aunque no todos los comandos tienen un valor significativo para devolver.
Repetir y terminar
¿Estoy captando correctamente por qué hay confusión sobre este tema? ¿Se me escapa algo?
Bueno, ¿los controladores de comando devuelven valores o no?
No deben devolver
datos comerciales
, solo metadatos (en relación con el éxito o el fracaso de la ejecución del comando).
CQRS
es
CQS
llevado a un nivel superior.
Incluso si usted rompiera las reglas del purista y devolviera algo, ¿qué devolvería?
En CQRS, el controlador de comandos es un método de un
application service
que carga el
aggregate
luego llama a un método en el
aggregate
luego persiste el
aggregate
.
La intención del controlador de comandos es modificar el
aggregate
.
No sabría qué devolver que sería independiente de la persona que llama.
Cada controlador de llamada que llama / cliente querría saber algo más sobre el nuevo estado.
Si la ejecución del comando está bloqueando (también conocido como síncrono), todo lo que necesita saber es si el comando se ejecutó correctamente o no. Luego, en una capa superior, consultaría exactamente lo que necesita saber sobre el estado de la nueva aplicación utilizando un modelo de consulta que se ajuste mejor a sus necesidades.
Piense lo contrario, si devuelve algo de un controlador de comandos, le da dos responsabilidades: 1. modificar el estado agregado y 2. consultar algún modelo de lectura.
En cuanto a la validación de comandos, existen al menos dos tipos de validación de comandos:
- Verificación de la sanidad del comando, que verifica que un comando tiene los datos correctos (es decir, una dirección de correo electrónico es válida); esto se hace antes de que el comando llegue al agregado, en el controlador de comandos (el servicio de aplicación) o en el constructor del comando;
- comprobación de invariantes de dominio, que se realiza dentro del agregado, después de que el comando alcanza el agregado (después de que se llama a un método en el agregado) y verifica que el agregado pueda mutar al nuevo estado.
Sin embargo, si vamos un poco más arriba, en la
Presentation layer
(es decir, un punto final
REST
), el cliente de la
Application layer
, podríamos devolver cualquier cosa y no romperemos las reglas porque los puntos finales están diseñados después de los casos de uso. exactamente lo que desea devolver después de ejecutar un comando, en cada caso de uso.
CQRS y CQS son como microservicios y descomposición de clases: la idea principal es la misma ("tienden a módulos cohesivos pequeños"), pero se encuentran en diferentes niveles semánticos.
El objetivo de CQRS es hacer que los modelos de escritura / lectura se separen; Tales detalles de bajo nivel, como el valor de retorno del método específico, son completamente irrelevantes.
Tome nota de la siguiente cita de Fowler :
El cambio que introduce CQRS es dividir ese modelo conceptual en modelos separados para actualización y visualización, a los que se refiere como Comando y Consulta, respectivamente, siguiendo el vocabulario de CommandQuerySeparation.
Se trata de modelos , no de métodos .
El controlador de comandos puede devolver cualquier cosa, excepto los modelos de lectura: estado (éxito / error), eventos generados (es el objetivo principal de los controladores de comandos, por cierto: generar eventos para el comando dado), errores. Los manejadores de comandos a menudo lanzan excepciones no verificadas, es un ejemplo de señales de salida de manejadores de comandos.
Además, el autor del término, Greg Young, dice que los comandos siempre están sincronizados (de lo contrario, se convierte en evento): https://groups.google.com/forum/#!topic/dddcqrs/xhJHVxDx2pM
Greg Young
en realidad dije que no existe un comando asincrónico :) es en realidad otro evento.
Respuesta para @Constantin Galbenu, me enfrenté al límite.
@Misanthrope ¿Y qué haces exactamente con esos eventos?
@Constantin Galbenu, en la mayoría de los casos no los necesito como resultado del comando, por supuesto. En algunos casos, necesito notificar al cliente en respuesta a esta solicitud de API.
Es extremadamente útil cuando:
- Debe notificar el error a través de eventos en lugar de excepciones. Ocurre generalmente cuando su modelo necesita ser guardado (por ejemplo, cuenta el número de intentos con código / contraseña incorrectos) incluso si ocurrió un error. Además, algunos tipos no usan excepciones para errores comerciales en absoluto: solo eventos ( http://andrzejonsoftware.blogspot.com/2014/06/custom-exceptions-or-domain-events.html ) No hay ninguna razón en particular pensar que arrojar excepciones comerciales desde el controlador de comandos está bien, pero devolver eventos de dominio no lo está .
- Cuando el evento ocurre solo con algunas circunstancias dentro de su raíz agregada.
Y puedo proporcionar un ejemplo para el segundo caso. Imagina que hacemos un servicio similar a Tinder, tenemos el comando LikeStranger. Este comando PUEDE resultar en StrangersWereMatched si nos gusta la persona que ya nos ha gustado antes. Necesitamos notificar al cliente móvil en respuesta si se produjo una coincidencia o no. Si solo desea verificar matchQueryService después del comando, puede encontrar una coincidencia allí, pero no hay garantía de que esa coincidencia se haya producido en este momento, porque A VECES Tinder muestra extraños ya coincidentes (probablemente, en áreas despobladas, tal vez inconsistencia, probablemente solo haya 2do dispositivo, etc.).
Verificar la respuesta si StrangersWereMatched realmente sucedió en este momento es tan sencillo:
$events = $this->commandBus->handle(new LikeStranger(...));
if ($events->contains(StrangersWereMatched::class)) {
return LikeApiResponse::matched();
} else {
return LikeApiResponse::unknown();
}
Sí, puede introducir el ID del comando, por ejemplo, y hacer que Match lea el modelo para mantenerlo:
// ...
$commandId = CommandId::generate();
$events = $this->commandBus->handle(
$commandId,
new LikeStranger($strangerWhoLikesId, $strangerId)
);
$match = $this->matchQueryService->find($strangerWhoLikesId, $strangerId);
if ($match->isResultOfCommand($commandId)) {
return LikeApiResponse::matched();
} else {
return LikeApiResponse::unknown();
}
... pero piénselo: ¿por qué cree que ese primer ejemplo con lógica directa es peor?
De todos modos, no viola CQRS, solo hice explícito lo implícito.
Es un enfoque inmutable sin estado.
Menos posibilidades de encontrar un error (por ejemplo, si
matchQueryService
se almacena en caché / se retrasa [no es coherente al instante], tiene un problema).
Sí, cuando el hecho de la coincidencia no es suficiente y necesita obtener datos para la respuesta, debe usar el servicio de consulta. Pero nada le impide recibir eventos del controlador de comandos.
Seguir el consejo de Vladimir Khononov en Tackling Complexity en CQRS sugiere que el manejo de comandos puede devolver información relacionada con su resultado.
Sin violar ningún principio [CQRS], un comando puede devolver de forma segura los siguientes datos:
- Resultado de ejecución: éxito o fracaso;
- Mensajes de error o errores de validación, en caso de falla;
- El nuevo número de versión del agregado, en caso de éxito;
Esta información mejorará dramáticamente la experiencia del usuario de su sistema, porque:
- No tiene que sondear una fuente externa para el resultado de ejecución del comando, lo tiene de inmediato. Se vuelve trivial validar comandos y devolver mensajes de error.
- Si desea actualizar los datos mostrados, puede usar la nueva versión del agregado para determinar si el modelo de vista refleja el comando ejecutado o no. No más mostrar datos obsoletos.
Daniel Whittaker aboga por devolver un objeto de " resultado común " de un controlador de comandos que contiene esta información.