Desajuste conceptual entre DDD Application Services y REST API

20

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:

  1. ¿Debería toda esta complejidad ser manejada por una capa de mapeo REST-to-AppService (gruesa)?
  2. ¿O me falta algo en mi comprensión de DDD / REST?
  3. ¿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?
astreltsov
fuente
3
Personalmente, no considero que REST sea tan necesario. Sin embargo, es posible calzar DDD en él: infoq.com/articles/rest-api-on-cqrs programmers.stackexchange.com/questions/242884/… blog.42.nl/articles/rest-and-ddd-incompatible
Den
Piense en el cliente REST como un usuario del sistema. No les importa en absoluto CÓMO el sistema realiza las acciones que realiza. No esperaría más que el cliente REST conozca todas las diferentes acciones en el dominio de lo que esperaría que un usuario lo supiera. Como usted dice, esta lógica tiene que ir a algún lado, pero tendría que ir a algún lado en cualquier sistema, si no estuviera usando REST, simplemente lo estaría moviendo hacia el cliente. No hacer eso es precisamente el punto de REST, el cliente solo debe saber que desea actualizar el estado y no debe tener idea de cómo hacerlo.
Cormac Mulhall
2
@astr La respuesta simple es que los recursos no son su modelo, por lo que el diseño del código de manejo de recursos no debería afectar el diseño de su modelo. Los recursos son un aspecto externo del sistema, donde el modelo es interno. Piense en los recursos de la misma manera que podría pensar en la IU. Un usuario puede hacer clic en un solo botón en la interfaz de usuario y suceden cientos de cosas diferentes en el modelo. Similar a un recurso. Un cliente actualiza un recurso (una sola declaración PUT) y podrían ocurrir un millón de cosas diferentes en el modelo. Es un antipatrón para acoplar su modelo estrechamente a sus recursos.
Cormac Mulhall
1
Esta es una buena charla sobre el tratamiento de las acciones en su dominio como efectos secundarios de los cambios de estado REST, manteniendo su dominio y la web separados (avance rápido a 25 minutos por un momento jugoso) yow.eventer.com/events/1004/talks/1047
Cormac Mulhall
1
Tampoco estoy seguro de todo el asunto "usuario como robot / máquina de estado". Creo que deberíamos esforzarnos para que nuestras interfaces de usuario sean mucho más naturales que eso ...
guillaume31

Respuestas:

10

Tuve el mismo problema y lo "resolví" modelando los recursos REST de manera diferente, por ejemplo:

/users/1  (contains basic user attributes) 
/users/1/email 
/users/1/activation 
/users/1/address

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.

qbd
fuente
Eso es lo que he estado pensando también: crear representaciones de recursos más granulares porque son más convenientes para las operaciones de escritura. ¿Cómo maneja la consulta de recursos cuando se vuelven tan granulares? ¿Crear también representaciones desnormalizadas de solo lectura?
astreltsov
1
No, no tengo representaciones desnormalizadas de solo lectura. Uso el estándar jsonapi.org y tiene un mecanismo para incluir recursos relacionados en la respuesta para un recurso dado. Básicamente digo "dame un usuario con ID 1 y también incluye su correo electrónico de subrecursos y activación". Esto ayuda a deshacerse de las llamadas REST adicionales para los recursos secundarios y no afecta la complejidad del cliente que trata con los recursos secundarios si utiliza una buena biblioteca de cliente API JSON.
qbd
Entonces, ¿una sola solicitud GET en el servidor se traduce en una o más consultas reales (dependiendo de cuántos sub-recursos están incluidos) que luego se combinan en un solo objeto de recurso?
astreltsov
¿Qué pasa si es necesario más de un nivel de anidamiento?
astreltsov
Sí, en dbs relacionales esto probablemente se traducirá en múltiples consultas. La anidación arbitraria es compatible con la API JSON, se describe aquí: jsonapi.org/format/#fetching-includes
qbd
0

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:

class User { 
      String name;
      String email;

      /**
       * This method will be transparently invoked when the value of name is changed
       * by REST.
       * The XorUpdate annotation becomes effective for PUT/POST actions
       */
      @XorPostChange
      public void validateName() {
        if(name == null) {
          throw new IllegalStateException("Name cannot be set as null");
        }
      }
    }

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.

codedabbler
fuente
0

Tengo algunos problemas para encontrar una manera de exponer el modelo de dominio de una manera orientada a los recursos.

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.

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.

Para nada: envíe los comandos a los recursos de la aplicación que interactúan con el modelo de dominio.

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

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.

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?

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?

  1. El cliente comienza con una descripción hipermedia del estado actual (es decir, la IU basada en tareas), incluidos los controles hipermedia.
  2. El cliente envía una representación de la tarea (es decir, el DTO) al recurso.
  3. El recurso analiza la solicitud HTTP entrante, toma la representación y la entrega a la aplicación.
  4. La aplicación ejecuta la tarea; desde el punto de vista del recurso, esta es una caja negra que tiene uno de los siguientes resultados
    • la aplicación actualizó con éxito todos los agregados: el recurso informa el éxito al cliente y lo dirige a un nuevo estado de aplicación
    • la capa anticorrupción rechaza el mensaje: el recurso informa un error 4xx al cliente (probablemente Solicitud incorrecta), posiblemente pasando una descripción del problema encontrado.
    • la aplicación actualiza algunos agregados: el recurso informa al cliente que el comando fue aceptado y dirige al cliente a un recurso que proporcionará una representación del progreso del comando.

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

VoiceOfUnreason
fuente
Si estamos diseñando la API para interactuar con nuestro dominio según nuestros casos de uso. ¿Por qué no diseñar las cosas de tal manera que no se requieran Sagas? Tal vez me estoy perdiendo algo, pero al leer su respuesta, realmente creo que REST no es una buena combinación con DDD y es mejor usar procedimientos remotos (RPC). DDD está centrado en el comportamiento, mientras que REST está centrado en el verbo http. ¿Por qué no eliminar REST de la imagen y exponer el comportamiento (comandos) en la API? Después de todo, probablemente fueron diseñados para satisfacer los casos de uso y los problemas son transaccionales. ¿Cuál es la ventaja de REST si poseemos la interfaz de usuario?
iberodev