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:
- Toute cette complexité doit-elle être gérée par une couche de mappage REST-AppService (épaisse)?
- Ou est-ce que je manque quelque chose dans ma compréhension de DDD / REST?
- 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é?
la source
Réponses:
J'ai eu le même problème et je l'ai "résolu" en modélisant les ressources REST différemment, par exemple:
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é.
la source
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:
Avec un tel outil à votre disposition, il vous suffit d'annoter vos méthodes de logique métier de manière appropriée.
la source
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.
Pas du tout - envoyez les commandes aux ressources d'application qui s'interfacent avec le modèle de domaine.
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.
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?
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 ddd , à 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.
la source