Implémentation du modèle de commande dans une API RESTful

12

Je suis en train de concevoir une API HTTP, je l'espère la rendre aussi RESTful que possible.

Il existe certaines actions dont la fonctionnalité est répartie sur quelques ressources et doit parfois être annulée.

Je me suis dit que cela ressemble à un modèle de commande, mais comment puis-je le modéliser en une ressource?

Je vais présenter une nouvelle ressource nommée XXAction, comme DepositAction, qui sera créée par quelque chose comme ça

POST /card/{card-id}/account/{account-id}/Deposit
AmountToDeposit=100, different parameters...

cela va en fait créer une nouvelle DepositAction et activer sa méthode Do / Execute. Dans ce cas, le retour d'un état HTTP 201 créé signifie que l'action a été exécutée avec succès.

Plus tard, si un client souhaite consulter les détails de l'action, il peut

GET /action/{action-id}

La mise à jour / PUT devrait être bloquée, je suppose, car elle n'est pas pertinente ici.

Et pour annuler l'action, j'ai pensé à utiliser

DELETE /action/{action-id}

qui appellera en fait la méthode Undo de l'objet concerné et changera son statut.

Disons que je suis content d'un seul Do-Undo, je n'ai pas besoin de refaire.

Cette approche est-elle correcte?

Y a-t-il des pièges, des raisons de ne pas l'utiliser?

Est-ce compris du POV des clients?

Mithir
la source
Réponse courte, ce n'est pas REST.
Evan Plaice
3
@EvanPlaice tient à élaborer sur ce point? c'est exactement la question.
Mithir
1
J'aurais élaboré dans une réponse, mais la réponse de Gary couvre déjà la plupart / tout ce que j'ajouterais. Je dis que ce n'est pas du repos car les URI ne sont censés représenter que des ressources (c'est-à-dire pas des actions). Les actions sont gérées via GET / POST / PUT / DELETE / HEAD. Considérez REST comme une interface OOP. L'objectif étant de faire en sorte que l'API corresponde au modèle général et le dissocie autant que possible des détails spécifiques à l'implémentation.
Evan Plaice
1
@EvanPlaice Ok je comprends, merci. Je pense que c'est déroutant ici parce que Deposit pourrait être considéré comme un nom et comme un verbe ...
Mithir
Dans ce cas, l'URI doit représenter une transaction où le débit (prendre de l'argent) et le crédit (donner de l'argent) sont des actions effectuées via des requêtes POST. POST est utilisé pour les deux car chaque fois que l'argent est déplacé dans les deux sens, il représente une nouvelle transaction en cours de création. Dans votre cas spécifique, les transactions ont lieu sur le compte d'un titulaire de carte, donc le numéro de compte de la carte est l'URI de la ressource.
Evan Plaice

Réponses:

13

Vous ajoutez une couche d'abstraction qui prête à confusion

Votre API démarre très propre et simple. Un HTTP POST crée une nouvelle ressource de dépôt avec les paramètres donnés. Ensuite, vous sortez des rails en introduisant l'idée d '"actions" qui sont un détail d'implémentation plutôt qu'une partie centrale de l'API.

Comme alternative, considérez cette conversation HTTP ...

POST / card / {card-id} / account / {account-id} / Deposit

AmountToDeposit = 100, différents paramètres ...

201 CRÉÉ

Lieu = / carte / 123 / compte / 456 / Caution / 789

Maintenant, vous voulez annuler cette opération (techniquement, cela ne devrait pas être autorisé dans un système comptable équilibré mais quoi de plus):

SUPPRIMER / carte / 123 / compte / 456 / Dépôt / 789

204 SANS CONTENU

Le consommateur d'API sait qu'il s'agit d'une ressource de dépôt et est en mesure de déterminer quelles opérations sont autorisées sur celle-ci (généralement via OPTIONS dans HTTP).

Bien que la mise en œuvre de l'opération de suppression soit effectuée via des "actions" aujourd'hui, il n'y a aucune garantie que lorsque vous migrez ce système de, disons, C # vers Haskell et maintenez le front-end que le concept secondaire d'une "action" continuerait à ajouter de la valeur , alors que le concept principal de dépôt le fait certainement.

Modifier pour couvrir une alternative à DELETE et Deposit

Afin d'éviter une opération de suppression, mais toujours de supprimer efficacement le dépôt, vous devez procéder comme suit (en utilisant une transaction générique pour permettre le dépôt et le retrait):

POST / card / {card-id} / account / {account-id} / Transaction

Montant = -100 , différents paramètres ...

201 CRÉÉ

Emplacement = / carte / 123 / compte / 456 / Transation / 790

Une nouvelle ressource Transaction est créée qui a exactement le montant opposé (-100). Cela a pour effet d'équilibrer le compte à 0, annulant la transaction d'origine.

Vous pourriez envisager de créer un point de terminaison "utilitaire" comme

POST / card / {card-id} / account / {account-id} / Transaction / 789 / Undo <- BAD!

pour obtenir le même effet. Cependant, cela rompt la sémantique d'un URI comme étant un identifiant en introduisant un verbe. Vous feriez mieux de vous en tenir aux noms dans les identificateurs et de limiter les opérations aux verbes HTTP. De cette façon, vous pouvez facilement créer un permalien à partir de l'identifiant et l'utiliser pour des GET et ainsi de suite.

Gary Rowe
la source
3
+1 "techniquement, cela ne devrait pas être autorisé dans un système comptable équilibré". Quelqu'un sait compter les haricots. Cette affirmation est tout à fait correcte, la façon d'inverser serait de créer une autre transaction créditant les fonds. Les écritures du grand livre général doivent toujours être considérées comme immuables et permanentes une fois la transaction terminée.
Evan Plaice
Donc, si je change, dans mes questions, au lieu de Supprimer / action / ... en Supprimer / déposer / ... ça va?
Mithir
2
@Mithir Je décrivais la règle comptable. Dans un système de comptabilité standard à double entrée, vous ne supprimez jamais les transactions. L'histoire une fois engagée est considérée comme immuable pour garder les gens honnêtes. Dans votre cas, vous pouvez toujours utiliser une action SUPPRIMER, mais sur le serveur principal (ex table de base de données du grand livre général), vous ajouteriez une autre transaction représentant le crédit (c.-à-d. Redonner) l'argent à l'utilisateur. Je ne suis pas un compteur de haricots (c'est-à-dire un comptable), mais c'est l'une des pratiques standard enseignées dans un cours "Principes de comptabilité I".
Evan Plaice
2
(suite) Les journaux de base de données utilisent les transactions de manière similaire. C'est pourquoi il est possible de répliquer et / ou de reconstruire un ensemble de données en utilisant uniquement les journaux. Tant que les transactions sont relues dans l'ordre chronologique, il devrait être possible de reconstruire l'ensemble de données à tout moment de son historique. La suppression de la mutabilité de l'équation garantit la cohérence.
Evan Plaice
1
Assez juste, renommez-le Transaction.
Gary Rowe
1

La principale raison de l'existence de REST est la résilience contre les erreurs de réseau. À quelle fin toutes les opérations doivent être idempotentes .

L'approche de base semble raisonnable, mais la façon dont vous décrivez la DepositActioncréation ne semble pas être idempotente, ce qui devrait être corrigé. En demandant au client de fournir un ID unique qui sera utilisé pour détecter les demandes en double. Donc, la création changerait pour

PUT /card/{card-id}/account/{account-id}/Deposit/{action-id}
AmountToDeposit=100, different parameters...

Si un autre PUT vers la même URL est effectué avec le même contenu que précédemment, la réponse doit toujours être 201 createdsi le contenu est le même et l'erreur si le contenu est différent. Cela permet au client de simplement retransmettre la demande en cas d'échec, car le client ne peut pas dire si la demande ou la réponse a été perdue.

Il est plus logique d'utiliser PUT, car il écrit simplement la ressource et est idempotent, mais l'utilisation de POST ne poserait pas vraiment de problème non plus.

Pour consulter les détails de la transaction, le client aura GETla même URL, c.-à-d.

GET /card/{card-id}/account/{account-id}/Deposit/{action-id}

et pour l'annuler, il peut le SUPPRIMER. Mais si cela a vraiment quelque chose à voir avec l'argent comme le suggère l'échantillon, je suggérerais de le mettre avec des drapeaux "annulés" ajoutés à la place pour la responsabilité (qu'il reste une trace de transaction créée et annulée).

Vous devez maintenant choisir une méthode de création de l'identifiant unique. Vous avez plusieurs options:

  1. Émettez un préfixe spécifique au client plus tôt dans l'échange qui doit être inclus.
  2. Ajoutez une demande POST spéciale pour obtenir un ID unique vierge du serveur. Cette demande n'a pas besoin d'être idempotente (et ne peut pas, vraiment), car les identifiants inutilisés ne causent vraiment aucun problème.
  3. Utilisez simplement UUID. Tout le monde les utilise et personne ne semble avoir de problème avec ceux basés sur MAC ni ceux aléatoires.
Jan Hudec
la source
2
D'après ce que je sais, POST n'est pas idempotent. en.wikipedia.org/wiki/POST_(HTTP)#Affecting_server_state
Mithir
@Mithir: POST n'est pas supposé être idempotent; ça peut encore l'être. Mais il est vrai que puisque toutes les opérations REST sont censées être idempotentes, POST n'a fondamentalement aucune place dans REST.
Jan Hudec
1
Je suis confus ... le contenu que j'ai lu et l'implémentation existante que je connais (ServiceStack, ASP.NET Web API), tous suggèrent que POST a une place dans REST.
Mithir
3
Dans REST, l'idempotence est attribuée à la ressource, pas au protocole ou à ses codes de réponse. Ainsi, dans REST sur HTTP, les méthodes GET, PUT, DELETE, PATCH et ainsi de suite sont considérées comme idempotentes bien que leurs codes de réponse puissent varier pour les appels suivants. POST est idempotent en ce sens que chaque appel crée une nouvelle ressource. Voir Fielding, il est OK d'utiliser POST .
Gary Rowe
1
Les opérations qui ne sont pas idempotentes sont autorisées au repos. Cette affirmation est carrément fausse.
Andy