Implementazione del modello di comando in un'API RESTful


12

Sto progettando un'API HTTP, spero di renderla il più RESIDENTE possibile.

Ci sono alcune azioni la cui funzionalità si estende su poche risorse e a volte deve essere annullata.

Ho pensato a me stesso, questo suona come un modello di comando, ma come posso modellarlo in una risorsa?

Introdurrò una nuova risorsa chiamata XXAction, come DepositAction, che verrà creata attraverso qualcosa del genere

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

questo creerà effettivamente un nuovo DepositAction e attiverà il suo metodo Do / Execute. In questo caso, la restituzione di uno stato HTTP 201 creato significa che l'azione è stata eseguita correttamente.

In seguito, se un cliente desidera esaminare i dettagli dell'azione, può farlo

GET /action/{action-id}

L'aggiornamento / PUT dovrebbe essere bloccato, suppongo, perché non è rilevante qui.

E per annullare l'azione, ho pensato di utilizzare

DELETE /action/{action-id}

che in realtà chiamerà il metodo Undo dell'oggetto rilevante e ne modificherà lo stato.

Diciamo che sono contento di un solo Do-Undo, non ho bisogno di ripetere.

Questo approccio va bene?

Ci sono insidie, ragioni per non usarlo?

Questo è compreso dal punto di vista dei clienti?


Risposta breve, non è RIPOSO.
Evan Plaice,

3
@EvanPlaice ti interessa approfondire? questa è esattamente la domanda.
Mithir,

1
Avrei elaborato una risposta, ma la risposta di Gary copre già quasi tutto ciò che aggiungerei. Dico che non è riposo perché gli URI dovrebbero rappresentare solo risorse (cioè non azioni). Le azioni vengono gestite tramite GET / POST / PUT / DELETE / HEAD. Pensa a REST come a un'interfaccia OOP. L'obiettivo è quello di adattare l'API al modello generale e disaccoppiarlo dai dettagli specifici di implementazione possibile.
Evan Plaice,

1
@EvanPlaice Ok, ho capito, grazie. Penso che sia confuso qui perché Deposit potrebbe essere pensato come un sostantivo e un verbo ...
Mithir

In questo caso l'URI dovrebbe rappresentare una transazione in cui l'addebito (prelevamento di denaro) e l'accredito (erogazione di denaro) sono azioni eseguite tramite richieste POST. Il POST viene utilizzato per entrambi perché ogni volta che il denaro viene spostato in entrambe le direzioni rappresenta una nuova transazione in fase di creazione. Nel tuo caso specifico, le transazioni avvengono sul conto di un titolare della carta, quindi il numero di conto della carta è l'URI della risorsa.
Evan Plaice,

Risposte:


13

Stai aggiungendo uno strato di astrazione che è confuso

La tua API inizia molto pulita e semplice. Un HTTP POST crea una nuova risorsa di deposito con i parametri indicati. Quindi vai fuori dai binari introducendo l'idea di "azioni" che sono un dettaglio di implementazione piuttosto che una parte fondamentale dell'API.

In alternativa, considera questa conversazione HTTP ...

POST / carta / {card-id} / account / {account-id} / deposito

AmountToDeposit = 100, parametri diversi ...

201 CREATO

Posizione = / scheda / 123 / account / 456 / deposito / 789

Ora vuoi annullare questa operazione (tecnicamente ciò non dovrebbe essere consentito in un sistema contabile equilibrato, ma quale ehi):

ELIMINA / carta / 123 / conto / 456 / deposito / 789

204 NESSUN CONTENUTO

Il consumatore dell'API sa di avere a che fare con una risorsa di deposito ed è in grado di determinare quali operazioni sono consentite su di essa (di solito tramite OPZIONI in HTTP).

Sebbene l'implementazione dell'operazione di eliminazione sia condotta oggi attraverso "azioni", non vi è alcuna garanzia che quando si migra questo sistema, diciamo, da C # a Haskell e si mantiene il front-end, il concetto secondario di "azione" continuerebbe ad aggiungere valore , mentre il concetto principale di deposito certamente lo fa.

Modifica per coprire un'alternativa a DELETE e Deposit

Al fine di evitare un'operazione di eliminazione, ma comunque rimuovere efficacemente il deposito, è necessario effettuare le seguenti operazioni (utilizzando una transazione generica per consentire il deposito e il prelievo):

POST / carta / {ID-carta} / account / {ID-conto} / Transazione

Importo = -100 , parametri diversi ...

201 CREATO

Posizione = / scheda / 123 / account / 456 / Transation / 790

Viene creata una nuova risorsa di transazione che ha esattamente l'importo opposto (-100). Ciò ha l'effetto di riportare il conto a 0, annullando la Transazione originale.

Si potrebbe prendere in considerazione la creazione di un endpoint di "utilità" come

POST / card / {card-id} / account / {account-id} / Transaction / 789 / Annulla <- MALE!

per ottenere lo stesso effetto. Tuttavia, ciò interrompe la semantica di un URI come identificatore introducendo un verbo. È meglio attenersi ai sostantivi negli identificatori e mantenere le operazioni vincolate ai verbi HTTP. In questo modo puoi facilmente creare un permalink dall'identificatore e usarlo per GET e così via.


3
+1 "tecnicamente ciò non dovrebbe essere consentito in un sistema contabile equilibrato". Qualcuno sa come contare i fagioli. Tale affermazione è assolutamente corretta, il modo per invertire sarebbe quello di creare un'altra transazione che accredita i fondi indietro. Le voci di contabilità generale devono sempre essere considerate immutabili e permanenti una volta completata una transazione.
Evan Plaice,

Quindi, se cambio, nelle mie domande, invece di Elimina / azione / ... in Elimina / deposito / ... va bene?
Mithir,

2
@Mithir Stavo descrivendo la regola contabile. In un sistema di contabilità standard a doppia entrata non si rimuovono mai le transazioni. La storia, una volta impegnata, è considerata immutabile per mantenere le persone oneste. Nel tuo caso potresti comunque usare un'azione DELETE ma sul back-end (ex tabella del database di contabilità generale) aggiungerei un'altra transazione che rappresenti l'accredito (ovvero la restituzione) dei soldi all'utente. Non sono un contatore di fagioli (vale a dire ragioniere) ma è una delle pratiche standard insegnate in un corso "Principi di contabilità I".
Evan Plaice,

2
(cont) I registri del database utilizzano le transazioni in modo simile. Ecco perché è possibile replicare e / o ricostruire un set di dati utilizzando solo i registri. Finché le transazioni vengono riprodotte cronologicamente, dovrebbe essere possibile ricostruire il set di dati da qualsiasi punto della sua storia. La rimozione della mutabilità dall'equazione garantisce coerenza.
Evan Plaice,

1
Abbastanza giusto basta rinominarlo in Transazione.
Gary Rowe,

1

Il motivo principale dell'esistenza di REST è la resilienza contro gli errori di rete. A tal fine, tutte le operazioni dovrebbero essere idempotenti .

L'approccio di base sembra ragionevole, ma il modo in cui descrivi la DepositActioncreazione non sembra idempotente, che dovrebbe essere risolto. Avendo il client fornire un ID univoco che verrà utilizzato per rilevare richieste duplicate. Quindi la creazione cambierebbe in

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

Se viene creato un altro PUT con lo stesso URL con lo stesso contenuto di prima, la risposta dovrebbe essere comunque 201 createdse il contenuto è lo stesso ed errore se il contenuto è diverso. Ciò consente al client di ritrasmettere semplicemente la richiesta quando fallisce, poiché il client non può dire se la richiesta o la risposta sono andate perse.

Ha più senso usare PUT, perché scrive solo la risorsa ed è idempotente, ma l'uso di POST non causerebbe davvero alcun problema.

Per esaminare i dettagli della transazione il cliente avrà GETlo stesso URL, ad es

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

e per annullarlo, può ELIMINARLO. Ma se in realtà ha qualcosa a che fare con il denaro come suggerisce il campione, suggerirei di metterlo con flag "cancellati" aggiunti invece per responsabilità (che rimane traccia della transazione creata e annullata).

Ora devi scegliere un metodo per creare l'ID univoco. Hai diverse opzioni:

  1. Emettere un prefisso specifico per il cliente in precedenza nello scambio che deve essere incluso.
  2. Aggiungi una richiesta POST speciale per ottenere un ID univoco vuoto dal server. Questa richiesta non deve essere idempotente (e non può, davvero), perché gli ID non utilizzati non causano realmente alcun problema.
  3. Usa semplicemente UUID. Tutti li usano e nessuno sembra avere alcun problema né con quelli basati su MAC né con quelli casuali.

2
Da quello che so, il POST non è idempotente. en.wikipedia.org/wiki/POST_(HTTP)#Affecting_server_state
Mithir

@Mithir: POST non è considerato idempotente; può ancora essere. Ma è vero che dal momento che tutte le operazioni REST dovrebbero essere idempotenti, POST non ha praticamente alcun posto in REST.
Jan Hudec,

1
Sono confuso ... il contenuto che ho letto e l'implementazione esistente con cui ho familiarità (ServiceStack, API Web ASP.NET), tutti suggeriscono che POST ha un posto in REST.
Mithir il

3
In REST l'idempotence è assegnato alla risorsa, non al protocollo o ai suoi codici di risposta. Pertanto, in REST su HTTP i metodi GET, PUT, DELETE, PATCH e così via sono considerati idempotenti sebbene i loro codici di risposta possano variare per le chiamate successive. POST è idempotente, nel senso che ogni chiamata crea una nuova risorsa. Vedi Fielding È OK usare POST .
Gary Rowe,

1
Le operazioni non idempotenti sono consentite a riposo. Questa affermazione è completamente sbagliata.
Andy,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.