Estoy tratando de diseñar una aplicación que tenga un dominio comercial complejo y un requisito para admitir una API REST (no estrictamente REST, sino orientada a los recursos). Tengo algunos problemas para encontrar una manera de exponer el modelo de dominio de una manera orientada a los recursos.
En DDD, los clientes de un modelo de dominio deben pasar por la capa de 'Servicios de aplicación' de procedimiento para acceder a cualquier funcionalidad comercial, implementada por Entidades y Servicios de dominio. Por ejemplo, hay un servicio de aplicación con dos métodos para actualizar una entidad de usuario:
userService.ChangeName(name);
userService.ChangeEmail(email);
La API de este Servicio de aplicaciones expone comandos (verbos, procedimientos), no estados.
Pero si también necesitamos proporcionar una API RESTful para la misma aplicación, entonces hay un modelo de recursos de usuario, que se ve así:
{
name:"name",
email:"[email protected]"
}
La API orientada a recursos expone el estado , no los comandos . Esto plantea las siguientes preocupaciones:
cada operación de actualización contra una API REST puede correlacionarse con una o más llamadas al procedimiento del Servicio de aplicaciones, según las propiedades que se actualicen en el modelo de recurso
cada operación de actualización parece atómica para el cliente REST API, pero no se implementa de esa manera. Cada llamada al Servicio de aplicaciones está diseñada como una transacción separada. Actualizar un campo en un modelo de recurso podría cambiar las reglas de validación para otros campos. Por lo tanto, debemos validar todos los campos del modelo de recursos juntos para asegurarnos de que todas las posibles llamadas al Servicio de aplicaciones sean válidas antes de comenzar a realizarlas. Validar un conjunto de comandos a la vez es mucho menos trivial que hacer uno a la vez. ¿Cómo hacemos eso en un cliente que ni siquiera sabe que existen comandos individuales?
llamar a los métodos de Application Service en un orden diferente podría tener un efecto diferente, mientras que REST API hace que parezca que no hay diferencia (dentro de un recurso)
Podría encontrar problemas más similares, pero básicamente todos son causados por lo mismo. Después de cada llamada a un Servicio de aplicaciones, el estado del sistema cambia. Reglas de lo que es un cambio válido, el conjunto de acciones que una entidad puede realizar el próximo cambio. Una API orientada a recursos intenta hacer que todo parezca una operación atómica. Pero la complejidad de cruzar esta brecha debe ir a alguna parte, y parece enorme.
Además, si la interfaz de usuario está más orientada a los comandos, que a menudo es el caso, entonces tendremos que asignar entre comandos y recursos en el lado del cliente y luego nuevamente en el lado de la API.
Preguntas:
- ¿Debería toda esta complejidad ser manejada por una capa de mapeo REST-to-AppService (gruesa)?
- ¿O me falta algo en mi comprensión de DDD / REST?
- ¿Podría REST simplemente no ser práctico para exponer la funcionalidad de los modelos de dominio en un cierto (bastante bajo) grado de complejidad?
fuente
Respuestas:
Tuve el mismo problema y lo "resolví" modelando los recursos REST de manera diferente, por ejemplo:
Así que básicamente he dividido el recurso más grande y complejo en varios más pequeños. Cada uno de estos contiene un grupo de atributos algo cohesivo del recurso original que se espera que se procesen juntos.
Cada operación en estos recursos es atómica, aunque puede implementarse usando varios métodos de servicio; al menos en Spring / Java EE no es un problema crear transacciones más grandes a partir de varios métodos que originalmente tenían la intención de tener su propia transacción (usando la transacción REQUERIDA propagación). A menudo todavía necesita hacer una validación adicional para este recurso especial, pero aún es bastante manejable ya que los atributos son (se supone que son) cohesivos.
Esto también es bueno para el enfoque HATEOAS, porque sus recursos más específicos transmiten más información sobre lo que puede hacer con ellos (en lugar de tener esta lógica tanto en el cliente como en el servidor porque no se puede representar fácilmente en los recursos).
Por supuesto, no es perfecto: si las IU no se modelan teniendo en cuenta estos recursos (especialmente las IU orientadas a datos), puede crear algunos problemas, por ejemplo, la IU presenta una gran forma de todos los atributos de los recursos dados (y sus recursos secundarios) y le permite edítelos todos y guárdelos a la vez; esto crea una ilusión de atomicidad a pesar de que el cliente debe llamar a varias operaciones de recursos (que son atómicas pero la secuencia completa no es atómica).
Además, esta división de recursos a veces no es fácil ni obvia. Hago esto principalmente en recursos con comportamientos complejos / ciclos de vida para administrar su complejidad.
fuente
La cuestión clave aquí es, ¿cómo se invoca la lógica empresarial de manera transparente cuando se realiza una llamada REST? Este es un problema que REST no aborda directamente.
He resuelto esto creando mi propia capa de gestión de datos sobre un proveedor de persistencia como JPA. Usando un metamodelo con anotaciones personalizadas, podemos invocar la lógica de negocios apropiada cuando cambia el estado de la entidad. Esto garantiza que, independientemente de cómo cambie el estado de la entidad, se invoque la lógica empresarial. Mantiene su arquitectura SECA y también su lógica empresarial en un solo lugar.
Usando el ejemplo anterior, podemos invocar un método de lógica de negocios llamado validateName cuando el campo de nombre se cambia usando REST:
Con dicha herramienta a su disposición, todo lo que tendrá que hacer es anotar sus métodos de lógica de negocios de manera adecuada.
fuente
No debería exponer el modelo de dominio de una manera orientada a los recursos. Debería exponer la aplicación de manera orientada a los recursos.
Para nada: envíe los comandos a los recursos de la aplicación que interactúan con el modelo de dominio.
Sí, aunque hay una forma ligeramente diferente de deletrear esto que puede simplificar las cosas; cada operación de actualización contra una API REST se asigna a un proceso que envía comandos a uno o más agregados.
Estás persiguiendo la cola equivocada aquí.
Imagínese: saque el resto completamente de la imagen. Imagine en cambio que estaba escribiendo una interfaz de escritorio para esta aplicación. Imaginemos además que tiene requisitos de diseño realmente buenos y está implementando una IU basada en tareas. Entonces, el usuario obtiene una interfaz minimalista que está perfectamente ajustada para la tarea que está trabajando; el usuario especifica algunas entradas y luego toca el "VERBO!" botón.
¿Que pasa ahora? Desde la perspectiva del usuario, esta es una tarea atómica única que debe realizarse. Desde la perspectiva de domainModel, se trata de una serie de comandos que se ejecutan mediante agregados, donde cada comando se ejecuta en una transacción separada. ¡Esos son completamente incompatibles! ¡Necesitamos algo en el medio para cerrar la brecha!
El algo es "la aplicación".
En el camino feliz, la aplicación recibe algo de DTO y analiza ese objeto para obtener un mensaje que entiende, y utiliza los datos en el mensaje para crear comandos bien formados para uno o más agregados. La aplicación se asegurará de que cada uno de los comandos que envía a los agregados estén bien formados (esa es la capa anticorrupción en funcionamiento), y cargará los agregados y guardará los agregados si la transacción se completa con éxito. El agregado decidirá por sí mismo si el comando es válido, dado su estado actual.
Resultados posibles: todos los comandos se ejecutan correctamente; la capa anticorrupción rechaza el mensaje; algunos de los comandos se ejecutan correctamente, pero uno de los agregados se queja y tiene una contingencia para mitigar.
Ahora, imagine que tiene esa aplicación integrada; ¿Cómo interactúas con él de una manera RESTANTE?
Se acepta la salida habitual cuando la aplicación va a diferir el procesamiento de un mensaje hasta después de responder al cliente, que se usa comúnmente al aceptar un comando asincrónico. Pero también funciona bien para este caso, donde una operación que se supone que es atómica necesita mitigación.
En este idioma, el recurso representa la tarea en sí misma: comienza una nueva instancia de la tarea publicando la representación apropiada en el recurso de la tarea, y ese recurso interactúa con la aplicación y lo dirige al siguiente estado de la aplicación.
En ddd , casi siempre que estás coordinando múltiples comandos, quieres pensar en términos de un proceso (también conocido como proceso de negocio, también conocido como saga).
Hay un desajuste conceptual similar en el modelo de lectura. Nuevamente, considere la interfaz basada en tareas; Si la tarea requiere modificar múltiples agregados, entonces la interfaz de usuario para preparar la tarea probablemente incluya datos de varios agregados. Si su esquema de recursos es 1: 1 con agregados, será difícil organizarlo; en su lugar, proporcione un recurso que devuelva una representación de los datos de varios agregados, junto con un control hipermedia que correlacione la relación de "tarea inicial" con el punto final de la tarea como se discutió anteriormente.
Ver también: REST in Practice de Jim Webber.
fuente