Perché puntare a un design RESTful?
I principi RESTful portano le funzionalità che semplificano i siti Web (per un utente umano casuale di "navigare") nella progettazione dell'API dei servizi Web , quindi sono facili da usare per un programmatore. REST non è buono perché è REST, è buono perché è buono. Ed è buono soprattutto perché è semplice .
La semplicità del semplice HTTP (senza buste SOAP e POSTservizi sovraccarichi URI singoli ), ciò che alcuni potrebbero chiamare "mancanza di funzionalità" , è in realtà il suo punto di forza . HTTP, subito, ti chiede di avere indirizzabilità e apolidia : le due decisioni di progettazione di base che mantengono scalabile HTTP fino ai mega-siti (e mega-servizi) di oggi.
Ma REST non è il bulltet d'argento: a volte uno stile RPC ("Remote Procedure Call" - come SOAP) può essere appropriato , e talvolta altre esigenze hanno la precedenza sulle virtù del Web. Questo va bene. Ciò che non ci piace è la complessità inutile . Troppo spesso un programmatore o un'azienda porta servizi in stile RPC per un lavoro che un semplice vecchio HTTP potrebbe gestire bene. L'effetto è che HTTP è ridotto a un protocollo di trasporto per un enorme payload XML che spiega cosa sta "realmente" succedendo (non l'URI o il metodo HTTP ne danno un'idea). Il servizio risultante è troppo complesso, impossibile da eseguire il debug e non funzionerà a meno che i client non abbiano la configurazione esatta come previsto dallo sviluppatore.
Allo stesso modo un codice Java / C # può essere non orientata agli oggetti, semplicemente utilizzando il protocollo HTTP non fare un disegno RESTful. Si può essere presi dalla fretta di pensare ai propri servizi in termini di azioni e metodi remoti che dovrebbero essere chiamati. Non sorprende che ciò finisca per lo più in un servizio in stile RPC (o un ibrido REST-RPC). Il primo passo è pensare diversamente. Un progetto RESTful può essere realizzato in molti modi, un modo è quello di pensare alla tua applicazione in termini di risorse, non di azioni:
💡 Invece di pensare in termini di azioni che può eseguire ("fai una ricerca di luoghi sulla mappa") ...
... prova a pensare in termini di risultati di tali azioni ("l'elenco dei luoghi sulla mappa che corrisponde a un criterio di ricerca").
Vado per esempi di seguito. (Un altro aspetto chiave di REST è l'uso di HATEOAS: non lo spazzolo qui, ma ne parlo rapidamente in un altro post .)
Problemi del primo disegno
Diamo un'occhiata al design proposto:
ACTION http://api.animals.com/v1/dogs/1/
Prima di tutto, non dovremmo considerare la creazione di un nuovo verbo HTTP ( ACTION). In generale, questo è indesiderabile per diversi motivi:
- (1) Dato solo l'URI del servizio, come farà un programmatore "casuale" a sapere che
ACTIONesiste il verbo?
- (2) se il programmatore sa che esiste, come farà a sapere la sua semantica? Cosa significa quel verbo?
- (3) quali proprietà (sicurezza, idempotenza) ci si dovrebbe aspettare da quel verbo?
- (4) cosa succede se il programmatore ha un client molto semplice che gestisce solo verbi HTTP standard?
- (5) ...
Ora prendiamo in considerazione l'usoPOST (discuterò il perché di seguito, prendi la mia parola ora):
POST /v1/dogs/1/ HTTP/1.1
Host: api.animals.com
{"action":"bark"}
Questo potrebbe essere OK ... ma solo se :
{"action":"bark"}era un documento; e
/v1/dogs/1/era un URI "elaboratore di documenti" (simile alla fabbrica). Un "elaboratore di documenti" è un URI che devi semplicemente "gettare le cose" e "dimenticare" su di esse - il processore può reindirizzare a una risorsa appena creata dopo il "lancio". Ad esempio l'URI per la pubblicazione di messaggi in un servizio di broker di messaggi, che, dopo la pubblicazione, ti reindirizzerebbe a un URI che mostra lo stato dell'elaborazione del messaggio.
Non so molto del tuo sistema, ma scommetto che entrambi non sono veri:
{"action":"bark"} non è un documento , in realtà è il metodo con cui stai cercando di intrufolarti nel servizio; e
- l'
/v1/dogs/1/URI rappresenta una risorsa "cane" (probabilmente il cane con id==1) e non un elaboratore di documenti.
Quindi tutto ciò che sappiamo ora è che il design sopra non è così RESTful, ma che cos'è esattamente? Cosa c'è di male? Fondamentalmente, è un male perché è un URI complesso con significati complessi. Non puoi dedurne nulla. Come farebbe un programmatore a sapere che un cane può avere barkun'azione che può essere segretamente infusa da un POSTdentro?
Progettare le chiamate API della tua domanda
Quindi andiamo al sodo e proviamo a progettare QUESTE abbaia pensando in termini di risorse . Consentitemi di citare il libro Restful Web Services :
Una POSTrichiesta è un tentativo di creare una nuova risorsa da una esistente. La risorsa esistente può essere il genitore di quello nuovo in un senso della struttura dei dati, il modo in cui la radice di un albero è il genitore di tutti i suoi nodi foglia. Oppure la risorsa esistente può essere una risorsa "factory" speciale il
cui unico scopo è generare altre risorse. La rappresentazione inviata insieme a una POSTrichiesta descrive lo stato iniziale della nuova risorsa. Come per PUT, una POSTrichiesta non deve necessariamente includere una rappresentazione.
Seguendo la descrizione sopra possiamo vedere che barkpuò essere modellato come una sottosorgente di undog (poiché un barkè contenuto in un cane, cioè una corteccia è "abbaiata" da un cane).
Da quel ragionamento abbiamo già ottenuto:
- Il metodo è
POST
- La risorsa è
/barks, subresource di dog:, che /v1/dogs/1/barksrappresenta una bark"fabbrica". Quell'URI è unico per ogni cane (dal momento che è sotto /v1/dogs/{id}).
Ora ogni caso del tuo elenco ha un comportamento specifico.
1. bark invia semplicemente un'e-mail a dog.emaile non registra nulla.
In primo luogo, abbaiare (inviare un'e-mail) è un'attività sincrona o asincrona? In secondo luogo, la barkrichiesta richiede qualche documento (l'e-mail, forse) o è vuota?
1.1 bark invia un'e-mail a dog.emaile non registra nulla (come attività sincrona)
Questo caso è semplice. Una chiamata alla barksrisorsa di fabbrica produce immediatamente una corteccia (una e-mail inviata) e la risposta (se OK o no) viene data immediatamente:
POST /v1/dogs/1/barks HTTP/1.1
Host: api.animals.com
Authorization: Basic mAUhhuE08u724bh249a2xaP=
(entity-body is empty - or, if you require a **document**, place it here)
200 OK
Poiché registra (cambia) nulla, 200 OKè sufficiente. Mostra che tutto è andato come previsto.
1.2 bark invia un'e-mail a dog.emaile non registra nulla (come attività asincrona)
In questo caso, il client deve avere un modo per tenere traccia barkdell'attività. L' barkattività quindi dovrebbe essere una risorsa con il proprio URI .:
POST /v1/dogs/1/barks HTTP/1.1
Host: api.animals.com
Authorization: Basic mAUhhuE08u724bh249a2xaP=
{document body, if needed;
NOTE: when possible, the response SHOULD contain a short hypertext note with a hyperlink
to the newly created resource (bark) URI, the same returned in the Location header
(also notice that, for the 202 status code, the Location header meaning is not
standardized, thus the importance of a hipertext/hyperlink response)}
202 Accepted
Location: http://api.animals.com/v1/dogs/1/barks/a65h44
In questo modo, ognuno barkè rintracciabile. Il client può quindi emettere una GETalla barkURI di sapere che è stato corrente. Forse anche usare a DELETEper annullarlo.
2. bark invia un'e-mail a dog.emaile quindi incrementa dog.barkCountdi 1
Questo può essere più complicato, se vuoi far sapere al cliente che la dogrisorsa viene cambiata:
POST /v1/dogs/1/barks HTTP/1.1
Host: api.animals.com
Authorization: Basic mAUhhuE08u724bh249a2xaP=
{document body, if needed; when possible, containing a hipertext/hyperlink with the address
in the Location header -- says the standard}
303 See Other
Location: http://api.animals.com/v1/dogs/1
In questo caso, l' locationintenzione dell'intestazione è far sapere al cliente che dovrebbe dare un'occhiata dog. Da HTTP RFC su303 :
Questo metodo esiste principalmente per consentire all'output di uno
POSTscript attivato di reindirizzare l'agente utente su una risorsa selezionata.
Se l'attività è asincrona, barkè necessaria una risorsa secondaria proprio come la 1.2situazione e 303deve essere restituita al GET .../barks/Ytermine dell'attività.
3. La corteccia crea un nuovo " bark" record con la bark.timestampregistrazione quando si è verificata la corteccia. Aumenta anche dog.barkCountdi 1.
POST /v1/dogs/1/barks HTTP/1.1
Host: api.animals.com
Authorization: Basic mAUhhuE08u724bh249a2xaP=
(document body, if needed)
201 Created
Location: http://api.animals.com/v1/dogs/1/barks/a65h44
Qui, barkviene creato a causa della richiesta, quindi lo stato 201 Createdviene applicato.
Se la creazione è asincrona, 202 Acceptedè invece necessario un ( come dice HTTP RFC ).
Il timestamp salvato è una parte della barkrisorsa e può essere recuperato con GETa. Il cane aggiornato può essere "documentato" anche in quello GET dogs/X/barks/Y.
4. bark esegue un comando di sistema per estrarre da Github l'ultima versione del codice del cane. Invia quindi un messaggio di testo per dog.ownerdire loro che il nuovo codice cane è in produzione.
La formulazione di questo è complicata, ma praticamente è un semplice compito asincrono:
POST /v1/dogs/1/barks HTTP/1.1
Host: api.animals.com
Authorization: Basic mAUhhuE08u724bh249a2xaP=
(document body, if needed)
202 Accepted
Location: http://api.animals.com/v1/dogs/1/barks/a65h44
Il client quindi invierebbe GETs per /v1/dogs/1/barks/a65h44conoscere lo stato corrente (se il codice è stato estratto, l'e-mail è stata inviata al proprietario e simili). Ogni volta che il cane cambia, a 303è applicabile.
Avvolgendo
Citando Roy Fielding :
L'unica cosa che REST richiede ai metodi è che siano definiti in modo uniforme per tutte le risorse (cioè, in modo che gli intermediari non debbano conoscere il tipo di risorsa per comprendere il significato della richiesta).
Negli esempi sopra, POSTè progettato in modo uniforme. Farà " bark" il cane . Ciò non è sicuro (il che significa che la corteccia ha effetti sulle risorse), né idempotente (ogni richiesta produce una nuova bark), che si adatta alPOST verbo.
Un programmatore dovrebbe sapere: a POSTper barkscedere a bark. I codici di stato della risposta (anche con corpo-entità e intestazioni quando necessario) svolgono il compito di spiegare cosa è cambiato e come il cliente può e deve procedere.
Nota: le fonti primarie utilizzate erano: il libro " Servizi Web riposanti ", il HTTP RFC e il blog di Roy Fielding .
Modificare:
La domanda e quindi la risposta sono cambiate un po 'da quando sono state create per la prima volta. La domanda originale è stata posta sulla progettazione di un URI come:
ACTION http://api.animals.com/v1/dogs/1/?action=bark
Di seguito è la spiegazione del perché non è una buona scelta:
Come i client dicono al server COSA FARE con i dati sono le informazioni sul metodo .
- I servizi Web RESTful trasmettono informazioni sul metodo nel metodo HTTP.
- I tipici servizi in stile RPC e SOAP mantengono il loro nel corpo dell'entità e nell'intestazione HTTP.
QUALE PARTE dei dati [il client vuole che il server] operi è l' informazione di scoping .
- I servizi RESTful utilizzano l'URI. I servizi in stile SOAP / RPC utilizzano ancora una volta le intestazioni body-body e HTTP.
Ad esempio, prendi l'URI di Google http://www.google.com/search?q=DOG. Lì, le informazioni sul metodo sono GETe le informazioni sull'ambito /search?q=DOG.
Per farla breve:
- Nelle architetture RESTful , le informazioni sul metodo vanno nel metodo HTTP.
- Nelle architetture orientate alle risorse , le informazioni di ambito vanno nell'URI.
E la regola empirica:
Se il metodo HTTP non corrisponde alle informazioni sul metodo, il servizio non è RESTful. Se le informazioni sull'ambito non si trovano nell'URI, il servizio non è orientato alle risorse.
Puoi inserire "corteccia" "azione" nell'URL (o nel corpo-entità) e utilizzarlo POST. Nessun problema, funziona e potrebbe essere il modo più semplice per farlo, ma questo non è RESTful .
Per mantenere il tuo servizio davvero RESTful, potresti dover fare un passo indietro e pensare a cosa vuoi veramente fare qui (quali effetti avrà sulle risorse).
Non posso parlare delle tue specifiche esigenze aziendali, ma lascia che ti dia un esempio: considera un servizio di ordinazione RESTful in cui gli ordini sono presso URI come example.com/order/123 .
Ora dì che vogliamo annullare un ordine, come lo faremo? Si può essere tentati di pensare che si tratti di un "annullamento" "azione" e progettarlo come POST example.com/order/123?do=cancel.
Non è RESTful, come abbiamo detto sopra. Invece, potremmo PUTuna nuova rappresentazione del ordercon un canceledelemento inviato a true:
PUT /order/123 HTTP/1.1
Content-Type: application/xml
<order id="123">
<customer id="89987">...</customer>
<canceled>true</canceled>
...
</order>
E questo è tutto. Se l'ordine non può essere annullato, è possibile restituire un codice di stato specifico. (Un design di risorse secondarie, come POST /order/123/canceledcon il corpo-entità, truepuò anche essere disponibile per semplicità.)
Nel tuo scenario specifico, potresti provare qualcosa di simile. In questo modo, mentre un cane abbaia, ad esempio, un GETat /v1/dogs/1/potrebbe includere tali informazioni (ad es<barking>true</barking> . ) . Oppure ... se è troppo complicato, allenta il tuo requisito RESTful e mantieni POST.
Aggiornare:
Non voglio rendere la risposta troppo grande, ma ci vuole un po 'di tempo prima di esporre un algoritmo ( un'azione ) come un insieme di risorse. Invece di pensare in termini di azioni ( "fai una ricerca di luoghi sulla mappa" ), devi pensare in termini di risultati di quell'azione ( "l'elenco dei luoghi sulla mappa che corrisponde a un criterio di ricerca" ).
Potresti ritrovarti a tornare a questo passaggio se scopri che il tuo design non si adatta all'interfaccia uniforme di HTTP.
Le variabili di query sono informazioni di ambito , ma non indicano nuove risorse ( /post?lang=enè chiaramente la stessa risorsa di /post?lang=jp, solo una rappresentazione diversa). Piuttosto, sono usati per comunicare lo stato del client (come ?page=10, in modo che lo stato non sia mantenuto nel server; qui ?lang=enè anche un esempio) o inserire parametri nelle risorse algoritmiche ( /search?q=dogs, /dogs?code=1). Ancora una volta, non risorse distinte.
Proprietà (metodi) dei verbi HTTP:
Un altro punto chiaro che mostra ?action=somethingnell'URI non è RESTful, sono le proprietà dei verbi HTTP:
GETe HEADsono sicuri (e idempotenti);
PUTe DELETEsono solo idempotenti;
POST non è nessuno dei due.
Sicurezza : una GETo una HEADrichiesta è una richiesta di lettura di alcuni dati, non una richiesta di modifica di qualsiasi stato del server. Il cliente può fare GETo HEADrichiedere 10 volte ed è lo stesso che farlo una volta, o mai farlo .
Idempotenza : un'operazione idempotente in una che ha lo stesso effetto se la applichi una o più volte (in matematica, moltiplicare per zero è idempotente). Se DELETEuna risorsa è una volta, l'eliminazione di nuovo avrà lo stesso effetto (la risorsa è GONEgià).
POSTnon è né sicuro né idempotente. Effettuare due POSTrichieste identiche a una risorsa "factory" comporterà probabilmente due risorse subordinate contenenti le stesse informazioni. Con sovraccarico (metodo in URI o corpo-entità) POST, tutte le scommesse sono disattivate.
Entrambe queste proprietà sono state importanti per il successo del protocollo HTTP (su reti inaffidabili!): Quante volte hai aggiornato (GET ) la pagina senza aspettare fino a quando non è completamente caricata?
La creazione di un'azione e il suo posizionamento nell'URL infrangono chiaramente il contratto dei metodi HTTP. Ancora una volta, la tecnologia ti consente, puoi farlo, ma non è un design RESTful.