web-dev-qa-db-fra.com

Reste API et DDD

Dans mon projet utilisant la méthodologie DDD.

Le projet a l'agrégat (entité) Deal. Cet agrégat a de nombreux cas d'utilisation.

Pour cet ensemble, j'ai besoin de créer une api de repos.

Avec standard: créer et supprimer aucun problème.

1) CreateDealUseCase (nom, prix et beaucoup d’autres paramètres);

POST /rest/{version}/deals/
{ 
   'name': 'deal123',
   'price': 1234;
   'etc': 'etc'
}

2) DeleteDealUseCase (id)

DELETE /rest/{version}/deals/{id}

Mais que faire avec le reste des cas d'utilisation?

  • HoldDealUseCase (id, raison);
  • UnholdDealUseCase (id);
  • CompleteDealUseCase (id et de nombreux autres paramètres);
  • CancelDealUseCase (id, amercement, raison);
  • ChangePriceUseCase (id, newPrice, raison);
  • ChangeCompletionDateUseCase (id, newDate, amercement, whyChanged);
  • etc (total 20 cas d'utilisation) ...

Quelles sont les solutions?

1) Utilisez des verbes :

PUT /rest/{version}/deals/{id}/hold
{ 
   'reason': 'test'
}

Mais! Les verbes ne peuvent pas être utilisés dans l'URL (dans la théorie REST).

2) Utilisez l'état terminé (qui sera après le cas d'utilisation):

PUT /rest/{version}/deals/{id}/holded
{ 
   'reason': 'test'
}

Personnellement pour moi c'est moche. J'ai peut-être tort?

3) Utilisez 1 requête PUT pour toutes les opérations:

PUT /rest/{version}/deals/{id}
{ 
   'action': 'HoldDeal',
   'params': {'reason': 'test'}
}

PUT /rest/{version}/deals/{id}
{ 
   'action': 'UnholdDeal',
   'params': {}
}

Il est difficile à gérer dans le backend ..__ De plus, il est difficile à documenter. Depuis 1 action a beaucoup de différentes variantes de demandes, dont dépend déjà des réponses spécifiques.

Toutes les solutions présentent des inconvénients importants.

J'ai lu de nombreux articles sur REST sur Internet. Partout seulement une théorie, comment être ici avec mon problème spécifique?

30
stalxed

J'ai lu de nombreux articles sur REST sur Internet.

D'après ce que je vois ici, vous devez vraiment regarder au moins une des conférences de Jim Webber sur REST et DDD.

Mais que faire avec le reste des cas d'utilisation?

Ignorer l'API pour un moment - comment le feriez-vous avec les formulaires HTML?

Vous auriez probablement une page Web qui présente une représentation de Deal, avec un tas de liens. Un lien vous mènerait au formulaire HoldDeal et un autre lien vous mènerait au formulaire ChangePrice, et ainsi de suite. Chacun de ces formulaires aurait zéro ou plusieurs champs à remplir, et chacun posterait une ressource pour mettre à jour le modèle de domaine.

Vont-ils tous publier sur la même ressource? Peut-être, peut-être pas. Ils auraient tous le même type de média. Par conséquent, s'ils publient tous sur le même point de terminaison Web, vous devrez décoder le contenu de l'autre côté.

Compte tenu de cette approche, comment implémentez-vous votre système? Eh bien, le type de média veut être json, basé sur vos exemples, mais il n’ya vraiment rien qui cloche dans le reste.

1) Utilisez des verbes:

C'est très bien.

Mais! Les verbes ne peuvent pas être utilisés dans l'URL (dans la théorie REST).

Um non. REST ne se soucie pas de l'orthographe de vos identificateurs de ressources. Il existe de nombreuses meilleures pratiques d'URI qui prétendent que les verbes sont mauvais - c'est vrai - mais ce n'est pas quelque chose qui découle de REST.

Mais si les gens sont si difficiles, vous nommez le point final de la commande au lieu du verbe. (ie: "hold" n'est pas un verbe, c'est un cas d'utilisation).

Utilisez 1 requête PUT pour toutes les opérations:

Honnêtement, celui-là n'est pas mal non plus. Cependant, vous ne voulez pas partager l'URI (en raison de la manière dont la méthode PUT est spécifiée), mais utilisez un modèle dans lequel les clients peuvent spécifier un identificateur unique.

Voici la viande: vous construisez une API sur les verbes HTTP et HTTP. HTTP est conçu pour transfert de documents. Le client vous fournit un document décrivant une modification demandée dans votre modèle de domaine, que vous appliquez la modification au domaine (ou non) et renvoyez un autre document décrivant le nouvel état.

En empruntant un instant le vocabulaire CQRS, vous postez des commandes pour mettre à jour votre modèle de domaine.

PUT /commands/{commandId}
{ 
   'deal' : dealId
   'action': 'HoldDeal',
   'params': {'reason': 'test'}
}

Justification - vous mettez une commande spécifique (une commande avec un identifiant spécifique) dans la file d'attente des commandes, qui est une collection.

PUT /rest/{version}/deals/{dealId}/commands/{commandId}
{ 
   'action': 'HoldDeal',
   'params': {'reason': 'test'}
}

Ouais, c'est bien aussi.

Jetez un autre coup d'œil à RESTBucks. C'est un protocole de café, mais toute l'API ne fait que passer de petits documents pour faire avancer la machine à états.

25
VoiceOfUnreason

Concevez votre api de repos indépendamment de la couche de domaine. 

L'un des concepts clés de la conception pilotée par domaine est le faible couplage entre vos différentes couches logicielles. Ainsi, lorsque vous concevez votre api de repos, vous pensez à la meilleure api de repos que vous puissiez avoir. Ensuite, le rôle de la couche d'application est d'appeler les objets de domaine pour effectuer le cas d'utilisation requis.

Je ne peux pas concevoir votre api de repos pour vous, parce que je ne sais pas ce que vous essayez de faire, mais voici quelques idées.

Si je comprends bien, vous avez une ressource Deal. Comme vous l'avez dit, la création/suppression est facile:

  • POST/reste/{version}/deals
  • DELETE/rest/{version}/deals/{id}.

Ensuite, vous voulez "tenir" un accord. Je ne sais pas ce que cela signifie, vous devez penser à ce que cela change dans la ressource "Deal". Cela change-t-il un attribut? Si oui, vous modifiez simplement la ressource Deal.

PUT/rest/{version}/deals/{id}

{
    ...
    held: true,
    holdReason: "something",
    ...
}

Cela ajoute-t-il quelque chose? Pouvez-vous avoir plusieurs prises sur un deal? Cela me semble que "tenir" est un nom. Si c'est moche, trouvez un meilleur nom.

POST/rest/{version}/deals/{id}/hold

{
    reason: "something"
}

une autre solution: oubliez la théorie REST. Si vous pensez que votre api serait plus claire, plus efficace, plus simple avec l’utilisation de verbes dans l’url, alors faîtes-le. Vous pouvez probablement trouver un moyen de l'éviter, mais si vous ne le pouvez pas, ne faites pas quelque chose de laid, simplement parce que c'est la norme.

Regardez l'API de Twitter : De nombreux développeurs disent que Twitter possède une API bien conçue. Tadaa, il utilise des verbes! Qui s'en soucie, tant que c'est cool et facile à utiliser?

Je ne peux pas concevoir votre api pour vous, vous êtes le seul à connaître vos cas d'utilisation, mais je vais répéter mes deux conseils:

  • Concevez l’API de repos par elle-même, puis utilisez la couche d’application pour appeler les objets de domaine appropriés dans le bon ordre. C'est exactement ce que la couche d'application est ici.
  • Ne suivez pas les normes et les théories aveuglément. Oui, vous devriez essayer de suivre les bonnes pratiques et les normes autant que possible, mais si vous ne pouvez pas les laisser ensuite (après mûre réflexion, bien sûr)
14
Kaidjin

L'article Exposer CQRS via une API RESTful est une approche détaillée abordant votre problème. Vous pouvez vérifier le prototype API . Quelques commentaires:

  • C'est une approche sophistiquée, vous n'avez donc probablement pas besoin de tout mettre en œuvre dans l'article: la concurrence d'accès Event Sourcing via l'ETag de HTTP et If-Match est une fonctionnalité "avancée".
  • C'est une approche avisée: le type de commande DDD est envoyé via l'en-tête de type de média, pas via le corps. Personnellement, je trouve cela intéressant ... mais je ne suis pas sûr de l'appliquer de cette façon
1
Mathieu François

Je sépare les cas d'utilisation (UC) en 2 groupes: commandes et requêtes (CQRS) et j'ai 2 contrôleurs REST (un pour les commandes et un autre pour les requêtes). Les ressources REST ne doivent pas nécessairement être des objets de modèle pour y effectuer des opérations CRUD à la suite de POST/GET/PUT/DELETE. Les ressources peuvent être n'importe quel objet que vous voulez. En effet, dans DDD, vous ne devez pas exposer le modèle de domaine aux contrôleurs.

(1) RestApiCommandController: Une méthode par cas d'utilisation de la commande. La ressource REST dans l'URI est le nom de la classe de commande. La méthode est toujours POST, car vous créez la commande, puis vous l'exécutez via un bus de commande (un médiateur dans mon cas). Le corps de la demande est un objet JSON qui mappe les propriétés de la commande (les arguments de l'UC).

Par exemple: http://localhost:8181/command/asignTaskCommand/

@RestController
@RequestMapping("/command")
public class RestApiCommandController {

private final Mediator mediator;    

@Autowired
public RestApiCommandController (Mediator mediator) {
    this.mediator = mediator;
}    

@RequestMapping(value = "/asignTaskCommand/", method = RequestMethod.POST)
public ResponseEntity<?> asignTask ( @RequestBody AsignTaskCommand asignTaskCommand ) {     
    this.mediator.execute ( asigTaskCommand );
    return new ResponseEntity ( HttpStatus.OK );
}

(2) RestApiQueryController: Une méthode par cas d'utilisation de la requête. Ici, la ressource REST dans l'URI est l'objet DTO que la requête renvoie (en tant qu'élément d'une collection, ou tout simplement un seul). La méthode est toujours GET et les paramètres de la requête UC sont des paramètres dans l'URI.

Par exemple: http://localhost:8181/query/asignedTask/1

@RestController
@RequestMapping("/query")
public class RestApiQueryController {

private final Mediator mediator;    

@Autowired
public RestApiQueryController (Mediator mediator) {
    this.mediator = mediator;
}    

@RequestMapping(value = "/asignedTask/{employeeId}", method = RequestMethod.GET)
public ResponseEntity<List<AsignedTask>> asignedTasksToEmployee ( @PathVariable("employeeId") String employeeId ) {

    AsignedTasksQuery asignedTasksQuery = new AsignedTasksQuery ( employeeId);
    List<AsignedTask> result = mediator.executeQuery ( asignedTasksQuery );
    if ( result==null || result.isEmpty() ) {
        return new ResponseEntity ( HttpStatus.NOT_FOUND );
    }
    return new ResponseEntity<List<AsignedTask>>(result, HttpStatus.OK);
} 

REMARQUE: Le médiateur appartient à la couche d'application DDD. Il s'agit de la limite UC, il recherche la commande/requête et exécute le service d'application approprié.

0
choquero70