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 POST
servizi 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
ACTION
esiste 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 bark
un'azione che può essere segretamente infusa da un POST
dentro?
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 POST
richiesta è 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 POST
richiesta descrive lo stato iniziale della nuova risorsa. Come per PUT, una POST
richiesta non deve necessariamente includere una rappresentazione.
Seguendo la descrizione sopra possiamo vedere che bark
può 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/barks
rappresenta 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.email
e non registra nulla.
In primo luogo, abbaiare (inviare un'e-mail) è un'attività sincrona o asincrona? In secondo luogo, la bark
richiesta richiede qualche documento (l'e-mail, forse) o è vuota?
1.1 bark invia un'e-mail a dog.email
e non registra nulla (come attività sincrona)
Questo caso è semplice. Una chiamata alla barks
risorsa 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.email
e non registra nulla (come attività asincrona)
In questo caso, il client deve avere un modo per tenere traccia bark
dell'attività. L' bark
attività 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 GET
alla bark
URI di sapere che è stato corrente. Forse anche usare a DELETE
per annullarlo.
2. bark invia un'e-mail a dog.email
e quindi incrementa dog.barkCount
di 1
Questo può essere più complicato, se vuoi far sapere al cliente che la dog
risorsa 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' location
intenzione 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
POST
script attivato di reindirizzare l'agente utente su una risorsa selezionata.
Se l'attività è asincrona, bark
è necessaria una risorsa secondaria proprio come la 1.2
situazione e 303
deve essere restituita al GET .../barks/Y
termine dell'attività.
3. La corteccia crea un nuovo " bark
" record con la bark.timestamp
registrazione quando si è verificata la corteccia. Aumenta anche dog.barkCount
di 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, bark
viene creato a causa della richiesta, quindi lo stato 201 Created
viene applicato.
Se la creazione è asincrona, 202 Accepted
è invece necessario un ( come dice HTTP RFC ).
Il timestamp salvato è una parte della bark
risorsa e può essere recuperato con GET
a. 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.owner
dire 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 GET
s per /v1/dogs/1/barks/a65h44
conoscere 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 POST
per barks
cedere 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 GET
e 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 PUT
una nuova rappresentazione del order
con un canceled
elemento 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/canceled
con il corpo-entità, true
può anche essere disponibile per semplicità.)
Nel tuo scenario specifico, potresti provare qualcosa di simile. In questo modo, mentre un cane abbaia, ad esempio, un GET
at /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=something
nell'URI non è RESTful, sono le proprietà dei verbi HTTP:
GET
e HEAD
sono sicuri (e idempotenti);
PUT
e DELETE
sono solo idempotenti;
POST
non è nessuno dei due.
Sicurezza : una GET
o una HEAD
richiesta è una richiesta di lettura di alcuni dati, non una richiesta di modifica di qualsiasi stato del server. Il cliente può fare GET
o HEAD
richiedere 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 DELETE
una risorsa è una volta, l'eliminazione di nuovo avrà lo stesso effetto (la risorsa è GONE
già).
POST
non è né sicuro né idempotente. Effettuare due POST
richieste 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.