Inadéquation conceptuelle entre DDD Application Services et REST API

20

J'essaie de concevoir une application qui a un domaine commercial complexe et une exigence pour prendre en charge une API REST (pas strictement REST, mais orientée vers les ressources). J'ai du mal à trouver un moyen d'exposer le modèle de domaine d'une manière orientée vers les ressources.

Dans DDD, les clients d'un modèle de domaine doivent passer par la couche procédurale «Services d'application» pour accéder à toutes les fonctionnalités métier mises en œuvre par les entités et les services de domaine. Par exemple, il existe un service d'application avec deux méthodes pour mettre à jour une entité utilisateur:

userService.ChangeName(name);
userService.ChangeEmail(email);

L'API de ce service d'application expose des commandes (verbes, procédures) et non des états.

Mais si nous devons également fournir une API RESTful pour la même application, il existe un modèle de ressource utilisateur, qui ressemble à ceci:

{
name:"name",
email:"[email protected]"
}

L'API orientée ressources expose l' état , pas les commandes . Cela soulève les préoccupations suivantes:

  • chaque opération de mise à jour par rapport à une API REST peut être mappée à un ou plusieurs appels de procédure Application Service, selon les propriétés mises à jour sur le modèle de ressource

  • chaque opération de mise à jour ressemble à atomique au client API REST, mais elle n'est pas implémentée comme ça. Chaque appel Application Service est conçu comme une transaction distincte. La mise à jour d'un champ sur un modèle de ressource peut modifier les règles de validation pour d'autres champs. Nous devons donc valider tous les champs du modèle de ressource ensemble pour nous assurer que tous les appels potentiels du service d'application sont valides avant de commencer à les effectuer. Valider un ensemble de commandes à la fois est beaucoup moins trivial que d'en faire une à la fois. Comment faire cela sur un client qui ne sait même pas qu'il existe des commandes individuelles?

  • appeler des méthodes Application Service dans un ordre différent peut avoir un effet différent, tandis que l'API REST donne l'impression qu'il n'y a pas de différence (au sein d'une ressource)

Je pourrais trouver des problèmes plus similaires, mais fondamentalement, ils sont tous causés par la même chose. Après chaque appel à un service d'application, l'état du système change. Les règles de ce qui est un changement valide, l'ensemble des actions qu'une entité peut effectuer le prochain changement. Une API orientée ressources essaie de faire ressembler le tout à une opération atomique. Mais la complexité de franchir cet écart doit aller quelque part, et cela semble énorme.

De plus, si l'interface utilisateur est plus orientée commande, ce qui est souvent le cas, nous devrons alors mapper entre les commandes et les ressources côté client, puis de nouveau côté API.

Des questions:

  1. Toute cette complexité doit-elle être gérée par une couche de mappage REST-AppService (épaisse)?
  2. Ou est-ce que je manque quelque chose dans ma compréhension de DDD / REST?
  3. REST pourrait-il simplement ne pas être pratique pour exposer la fonctionnalité des modèles de domaine sur un certain degré (assez faible) de complexité?
astreltsov
la source
3
Personnellement, je ne considère pas REST comme nécessaire. Cependant, il est possible d'y chausse-pied DDD: infoq.com/articles/rest-api-on-cqrs programmers.stackexchange.com/questions/242884/… blog.42.nl/articles/rest-and-ddd-incompatible
Den
Considérez le client REST comme un utilisateur du système. Ils ne se soucient absolument de COMMENT le système exécute les actions qu'il effectue. Vous ne vous attendriez pas plus à ce que le client REST connaisse toutes les différentes actions sur le domaine que vous ne le pensez à un utilisateur. Comme vous le dites, cette logique doit aller quelque part, mais elle devrait aller quelque part dans n'importe quel système, si vous n'utilisiez pas REST, vous la déplaceriez simplement vers le client. Ne pas le faire est précisément le but de REST, le client doit seulement savoir qu'il veut mettre à jour l'état et ne doit avoir aucune idée de la façon dont vous procédez.
Cormac Mulhall
2
@astr La réponse simple est que les ressources ne sont pas votre modèle, donc la conception du code de gestion des ressources ne doit pas affecter la conception de votre modèle. Les ressources sont un aspect extérieur du système, alors que le modèle est interne. Pensez aux ressources de la même manière que vous pourriez penser à l'interface utilisateur. Un utilisateur peut cliquer sur un seul bouton de l'interface utilisateur et une centaine de choses différentes se produisent dans le modèle. Similaire à une ressource. Un client met à jour une ressource (une seule instruction PUT) et un million de choses différentes peuvent se produire dans le modèle. C'est un anti-modèle pour coupler votre modèle étroitement à vos ressources.
Cormac Mulhall
1
Ceci est une bonne discussion sur le traitement des actions dans votre domaine comme des effets secondaires des changements d'état REST, en gardant votre domaine et le Web séparés (avance rapide jusqu'à 25 min pour un peu juteux) yow.eventer.com/events/1004/talks/1047
Cormac Mulhall
1
Je ne suis pas sûr non plus de tout "l'utilisateur en tant que robot / machine d'état". Je pense que nous devrions nous efforcer de rendre nos interfaces utilisateur beaucoup plus naturelles que cela ...
guillaume31

Réponses:

10

J'ai eu le même problème et je l'ai "résolu" en modélisant les ressources REST différemment, par exemple:

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

J'ai donc essentiellement divisé la ressource plus grande et complexe en plusieurs plus petites. Chacun de ces éléments contient un groupe d'attributs quelque peu cohérent de la ressource d'origine qui devrait être traitée ensemble.

Chaque opération sur ces ressources est atomique, même si elle peut être implémentée à l'aide de plusieurs méthodes de service - au moins dans Spring / Java EE, il n'est pas difficile de créer une transaction plus importante à partir de plusieurs méthodes qui étaient initialement destinées à avoir leur propre transaction (en utilisant la transaction REQUISE) propagation). Vous devez souvent encore effectuer une validation supplémentaire pour cette ressource spéciale, mais elle est toujours assez gérable car les attributs sont (censés être) cohérents.

C'est également bon pour l'approche HATEOAS, car vos ressources plus fines transmettent plus d'informations sur ce que vous pouvez en faire (au lieu d'avoir cette logique sur le client et le serveur car elle ne peut pas être facilement représentée dans les ressources).

Ce n'est pas parfait bien sûr - si les interfaces utilisateur ne sont pas modélisées avec ces ressources à l'esprit (en particulier les interfaces utilisateur orientées données), cela peut créer des problèmes - par exemple, l'interface utilisateur présente une grande forme de tous les attributs des ressources données (et de ses sous-ressources) et vous permet de éditez-les tous et enregistrez-les à la fois - cela crée une illusion d'atomicité même si le client doit appeler plusieurs opérations de ressource (qui sont elles-mêmes atomiques mais la séquence entière n'est pas atomique).

De plus, cette répartition des ressources n'est parfois ni facile ni évidente. Je le fais principalement sur des ressources aux comportements / cycles de vie complexes pour gérer sa complexité.

qbd
la source
C'est ce que j'ai également pensé - créer des représentations de ressources plus granulaires car elles sont plus pratiques pour les opérations d'écriture. Comment gérez-vous l'interrogation des ressources lorsqu'elles deviennent si granulaires? Vous créez également des représentations dénormalisées en lecture seule?
astreltsov
1
Non, je n'ai pas de représentations dénormalisées en lecture seule. J'utiliser jsonapi.org standard et il dispose d' un mécanisme permettant d' inclure les ressources connexes dans la réponse de ressource donnée. Fondamentalement, je dis "donnez-moi l'utilisateur avec l'ID 1 et incluez également son e-mail de sous-ressources et son activation". Cela aide à se débarrasser des appels REST supplémentaires pour les sous-ressources et cela n'affecte pas la complexité du client traitant les sous-ressources si vous utilisez une bonne bibliothèque cliente de l'API JSON.
qbd
Ainsi, une seule requête GET sur le serveur se traduit par une ou plusieurs requêtes réelles (selon le nombre de sous-ressources incluses) qui sont ensuite combinées en un seul objet ressource?
astreltsov
Et si plus d'un niveau d'imbrication est nécessaire?
astreltsov
Oui, dans les dbs relationnels, cela se traduira probablement par plusieurs requêtes. L'imbrication arbitraire est prise en charge par l'API JSON, elle est décrite ici: jsonapi.org/format/#fetching-includes
qbd
0

La question clé ici est la suivante: comment la logique métier est-elle invoquée de manière transparente lors d'un appel REST? Il s'agit d'un problème qui n'est pas directement traité par REST.

J'ai résolu ce problème en créant ma propre couche de gestion des données sur un fournisseur de persistance tel que JPA. À l'aide d'un méta-modèle avec des annotations personnalisées, nous pouvons invoquer la logique métier appropriée lorsque l'état de l'entité change. Cela garantit que, quelle que soit la façon dont l'état de l'entité change, la logique métier est invoquée. Il garde votre architecture SECHE et aussi votre logique métier en un seul endroit.

En utilisant l'exemple ci-dessus, nous pouvons invoquer une méthode de logique métier appelée validateName lorsque le champ de nom est modifié à l'aide de 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");
        }
      }
    }

Avec un tel outil à votre disposition, il vous suffit d'annoter vos méthodes de logique métier de manière appropriée.

codedabbler
la source
0

J'ai du mal à trouver un moyen d'exposer le modèle de domaine d'une manière orientée vers les ressources.

Vous ne devez pas exposer le modèle de domaine d'une manière orientée vers les ressources. Vous devez exposer l'application de manière orientée ressources.

si l'interface utilisateur est plus orientée commande, ce qui est souvent le cas, nous devrons mapper entre les commandes et les ressources côté client, puis de nouveau du côté API.

Pas du tout - envoyez les commandes aux ressources d'application qui s'interfacent avec le modèle de domaine.

chaque opération de mise à jour par rapport à une API REST peut être mappée à un ou plusieurs appels de procédure Application Service, selon les propriétés mises à jour sur le modèle de ressource

Oui, bien qu'il existe une façon légèrement différente de l'énoncer qui puisse simplifier les choses; chaque opération de mise à jour par rapport à une API REST est mappée à un processus qui envoie des commandes à un ou plusieurs agrégats.

chaque opération de mise à jour ressemble à atomique au client API REST, mais elle n'est pas implémentée comme ça. Chaque appel Application Service est conçu comme une transaction distincte. La mise à jour d'un champ sur un modèle de ressource peut modifier les règles de validation pour d'autres champs. Nous devons donc valider tous les champs du modèle de ressource ensemble pour nous assurer que tous les appels potentiels du service d'application sont valides avant de commencer à les effectuer. Valider un ensemble de commandes à la fois est beaucoup moins trivial que d'en faire une à la fois. Comment faire cela sur un client qui ne sait même pas qu'il existe des commandes individuelles?

Vous poursuivez la mauvaise queue ici.

Imaginez: retirez complètement REST de l'image. Imaginez plutôt que vous écriviez une interface de bureau pour cette application. Imaginons en outre que vous avez de très bonnes exigences de conception et que vous implémentez une interface utilisateur basée sur les tâches. Ainsi, l'utilisateur obtient une interface minimaliste parfaitement adaptée à la tâche sur laquelle il travaille; l'utilisateur spécifie quelques entrées puis frappe le "VERBE!" bouton.

Que se passe-t-il maintenant? Du point de vue de l'utilisateur, il s'agit d'une tâche atomique unique à effectuer. Du point de vue du domainModel, il s'agit d'un certain nombre de commandes exécutées par des agrégats, où chaque commande est exécutée dans une transaction distincte. Ce sont complètement incompatibles! Nous avons besoin de quelque chose au milieu pour combler l'écart!

Le quelque chose est "l'application".

Sur la bonne voie, l'application reçoit du DTO, analyse cet objet pour obtenir un message qu'il comprend et utilise les données du message pour créer des commandes bien formées pour un ou plusieurs agrégats. L'application s'assurera que chacune des commandes qu'elle envoie aux agrégats est bien formée (c'est la couche anti-corruption au travail), et elle chargera les agrégats et enregistrera les agrégats si la transaction se termine avec succès. L'agrégat décidera par lui-même si la commande est valide, compte tenu de son état actuel.

Résultats possibles - les commandes s'exécutent toutes avec succès - la couche anti-corruption rejette le message - certaines commandes s'exécutent avec succès, mais l'un des agrégats se plaint et vous avez une contingence à atténuer.

Maintenant, imaginez que vous avez construit cette application; Comment interagissez-vous avec elle de manière RESTful?

  1. Le client commence par une description hypermédia de son état actuel (c'est-à-dire: l'interface utilisateur basée sur la tâche), y compris les contrôles hypermédia.
  2. Le client envoie une représentation de la tâche (c'est-à-dire: le DTO) à la ressource.
  3. La ressource analyse la demande HTTP entrante, capture la représentation et la transmet à l'application.
  4. L'application exécute la tâche; du point de vue de la ressource, il s'agit d'une boîte noire qui a l'un des résultats suivants
    • l'application a mis à jour avec succès tous les agrégats: la ressource signale le succès au client, la dirigeant vers un nouvel état d'application
    • la couche anti-corruption rejette le message: la ressource signale une erreur 4xx au client (probablement Bad Request), transmettant éventuellement une description du problème rencontré.
    • l'application met à jour certains agrégats: la ressource signale au client que la commande a été acceptée et dirige le client vers une ressource qui fournira une représentation de la progression de la commande.

Accepté est le cop-out habituel lorsque l'application va différer le traitement d'un message jusqu'à ce qu'il ait répondu au client - couramment utilisé lors de l'acceptation d'une commande asynchrone. Mais cela fonctionne également bien dans ce cas, où une opération supposée atomique doit être atténuée.

Dans cet idiome, la ressource représente la tâche elle-même - vous démarrez une nouvelle instance de la tâche en publiant la représentation appropriée dans la ressource de tâche, et cette ressource s'interface avec l'application et vous dirige vers l'état d'application suivant.

Dans , à peu près à chaque fois que vous coordonnez plusieurs commandes, vous voulez penser en termes de processus (aka processus métier, aka saga).

Il existe un décalage conceptuel similaire dans le modèle de lecture. Encore une fois, considérez l'interface basée sur les tâches; si la tâche nécessite de modifier plusieurs agrégats, l'interface utilisateur pour la préparation de la tâche comprend probablement des données provenant d'un certain nombre d'agrégats. Si votre schéma de ressources est 1: 1 avec des agrégats, cela va être difficile à organiser; au lieu de cela, fournissez une ressource qui renvoie une représentation des données de plusieurs agrégats, ainsi qu'un contrôle hypermédia qui mappe la relation de «tâche de démarrage» avec le point de terminaison de la tâche, comme expliqué ci-dessus.

Voir aussi: REST en pratique par Jim Webber.

VoiceOfUnreason
la source
Si nous concevons l'API pour interagir avec notre domaine selon nos cas d'utilisation. Pourquoi ne pas concevoir les choses de manière à ce que les Sagas ne soient pas du tout nécessaires? Peut-être que je manque quelque chose, mais en lisant votre réponse, je crois vraiment que REST n'est pas une bonne correspondance avec DDD et qu'il est préférable d'utiliser des procédures à distance (RPC). DDD est centré sur le comportement tandis que REST est centré sur le verbe http. Pourquoi ne pas supprimer REST de l'image et exposer le comportement (commandes) dans l'API? Après tout, ils ont probablement été conçus pour satisfaire les scénarios d'utilisation et les prob sont transactionnels. Quel est l'avantage de REST si nous possédons l'interface utilisateur?
iberodev