Pattern di progettazione comuni

Risposte vuote

Il metodo Delete standard deve restituire google.protobuf.Empty, a meno che non stia eseguendo un'eliminazione "soft", nel qual caso il metodo deve restituire la risorsa con lo stato aggiornato per indicare l'eliminazione in corso.

Per i metodi personalizzati, devono avere i propri messaggi XxxResponse anche se sono vuote, perché è molto probabile che le loro funzionalità aumenteranno e devono restituire dati aggiuntivi.

Rappresentazione degli intervalli

I campi che rappresentano intervalli devono utilizzare intervalli semiaperti con convenzione di denominazione [start_xxx, end_xxx), ad esempio [start_key, end_key) o [start_time, end_time). La semantica degli intervalli semiaperti è comunemente utilizzata dalla libreria STL di C++ e dalla libreria standard di Java. Le API devono evitare di utilizzare altri modi per rappresentare come (index, count) o [first, last].

Etichette risorse

In un'API orientata alle risorse, lo schema delle risorse è definito dall'API. Per consentire al client di associare una piccola quantità di metadati semplici alle risorse (ad esempio, il tagging di una risorsa di macchine virtuali come server di database), le API devono aggiungere un campo map<string, string> labels alla definizione della risorsa:

message Book {
  string name = 1;
  map<string, string> labels = 2;
}

Operazioni a lunga esecuzione

Se in genere un metodo API richiede molto tempo per essere completato, progettato per restituire al client una risorsa per un'operazione a lunga esecuzione, che il cliente può utilizzare per monitorare i progressi e ricevere il risultato. La Operazione definisce un'interfaccia standard per lavorare con operazioni a lunga esecuzione. Le singole API non devono definire le proprie interfacce per le operazioni di lunga durata per evitare incoerenze.

La risorsa dell'operazione deve essere restituita direttamente come messaggio di risposta ed eventuali conseguenze immediate dell'operazione devono essere riportate nell'API. Ad esempio, quando crei una risorsa, questa dovrebbe essere visualizzata nei metodi LIST e GET, anche se dovrebbe indicare che non è pronta per l'uso. Al termine dell'operazione, il campo Operation.response deve contenere il messaggio che sarebbe stato restituito direttamente, se il metodo non avesse richiesto molto tempo.

Un'operazione può fornire informazioni sul relativo avanzamento utilizzando il campo Operation.metadata. Un'API deve definire un messaggio per questi metadati anche se l'implementazione iniziale non compila il campo metadata.

Paginazione dell'elenco

Le raccolte visualizzabili devono supportare l'impaginazione, anche se i risultati tipicamente piccole.

Motivazione:se un'API non supporta l'impaginazione fin dall'inizio, il supporto in seguito può creare problemi perché l'aggiunta dell'impaginazione interrompe il comportamento dell'API. I client che non sanno che l'API ora utilizza la paginazione potrebbero erroneamente assumere di aver ricevuto un risultato completo, quando in realtà hanno ricevuto solo la prima pagina.

Per supportare la paginazione (restituzione dei risultati dell'elenco in pagine) in un metodo List , l'API deve:

  • definisci un campo string page_token nella richiesta del metodo List per creare un nuovo messaggio email. Il client utilizza questo campo per richiedere una pagina specifica dei risultati dell'elenco.
  • definisci un campo int32 page_size nella richiesta del metodo List per creare un nuovo messaggio email. I client utilizzano questo campo per specificare il numero massimo di risultati da restituire dal server. Il server potrebbe ulteriormente vincolare il numero massimo di risultati restituiti in una singola pagina. Se page_size è 0, il server decide il numero di risultati da restituire.
  • definisci un campo string next_page_token nel metodo List messaggio di risposta. Questo campo rappresenta il token di paginazione per recuperare la pagina di risultati successiva. Se il valore è "", significa che non ci sono altri risultati per la richiesta.

Per recuperare la pagina successiva dei risultati, il cliente deve passare il valore di next_page_token della risposta nella successiva chiamata al metodo List (in nel campo page_token del messaggio di richiesta):

rpc ListBooks(ListBooksRequest) returns (ListBooksResponse);

message ListBooksRequest {
  string parent = 1;
  int32 page_size = 2;
  string page_token = 3;
}

message ListBooksResponse {
  repeated Book books = 1;
  string next_page_token = 2;
}

Quando i client passano parametri di query oltre a un token di pagina, il servizio deve non soddisfare la richiesta se i parametri di query non sono coerenti con il token di pagina.

I contenuti del token della pagina devono essere un buffer di protocollo con codifica Base64 senza URL. In questo modo, i contenuti possono evolversi senza problemi di compatibilità. Se il token pagina contiene informazioni potenzialmente sensibili, queste devono essere criptate. I servizi devono impedire che la manipolazione dei token pagina esponga dati indesiderati tramite uno dei seguenti metodi:

  • richiedono la ridefinizione dei parametri di query nelle richieste successive.
  • fare riferimento solo allo stato della sessione lato server nel token della pagina.
  • crittografare e firmare i parametri di ricerca nel token di pagina e riconvalidare e autorizzare di nuovo questi parametri a ogni chiamata.

Un'implementazione della paginazione può anche fornire il conteggio totale degli elementi in un campo int32 denominato total_size.

Elenco di raccolte secondarie

A volte, un'API deve consentire a un client List/Search di raccolte. Ad esempio, l'API Library ha una raccolta di scaffali, e ogni scaffale ha una raccolta di libri e un cliente vuole cercare per un libro in tutti gli scaffali. In questi casi, consigliamo di utilizzare List standard nella raccolta secondaria e specificare il carattere jolly ID raccolta "-" per le raccolte principali. Per l'API Library Ad esempio, possiamo utilizzare la seguente richiesta API REST:

GET https://library.googleapis.com/v1/shelves/-/books?filter=xxx

Ottieni risorsa univoca da sottoraccolta

A volte, una risorsa all'interno di una sottoraccolta ha un identificatore unico all'interno delle collezioni principali. In questo caso, può essere utile per consentire a un Get di recuperare quella risorsa senza sapere quale padre che lo contiene. In questi casi, ti consigliamo di utilizzare una Get standard per la risorsa e specifica l'ID raccolta con caratteri jolly "-" per tutte le raccolte padre all'interno delle quali la risorsa è unica. Ad esempio, nell'API Library, possiamo utilizzare la seguente richiesta dell'API REST se il libro è unico tra tutti i libri di tutte le sezioni:

GET https://library.googleapis.com/v1/shelves/-/books/{id}

Il nome della risorsa nella risposta a questa chiamata deve utilizzare il nome canonico della risorsa, con gli identificatori effettivi delle collezioni principali anziché "-" per ogni collezione principale. Ad esempio, la richiesta precedente deve restituire un risorsa con un nome come shelves/shelf713/books/book8141, non shelves/-/books/book8141.

Ordinamento

Se un metodo API consente al client di specificare l'ordinamento per i risultati dell'elenco, il messaggio di richiesta deve contenere un campo:

string order_by = ...;

Il valore della stringa deve seguire la sintassi SQL: elenco separato da virgole di campi. Ad esempio: "foo,bar". L'ordinamento predefinito è crescente. Per specificare l'ordinamento decrescente per un campo, al nome del campo deve essere aggiunto un suffisso " desc" deve. Ad esempio: "foo desc,bar".

I caratteri di spaziatura ridondanti nella sintassi sono irrilevanti. "foo,bar desc" e "  foo ,  bar  desc  " sono equivalenti.

Richiedi convalida

Se un metodo API ha effetti collaterali ed è necessario convalidare la richiesta senza causarli, il messaggio di richiesta deve contenere un campo:

bool validate_only = ...;

Se questo campo viene impostato su true, il server non deve eseguire nessun lato ed eseguire la convalida specifica dell'implementazione in modo coerente con la richiesta completa.

Se la convalida va a buon fine, google.rpc.Code.OK deve essere restituito e qualsiasi richiesta completa che utilizza lo stesso messaggio di richiesta non deve restituire google.rpc.Code.INVALID_ARGUMENT. Tieni presente che la richiesta potrebbe comunque non riuscire a causa di altri errori come google.rpc.Code.ALREADY_EXISTS o perché delle condizioni di gara.

Richiedi duplicazione

Per le API di rete, i metodi API idempotenti sono molto preferiti, perché possono da riprovare in sicurezza in caso di errori di rete. Tuttavia, alcuni metodi dell'API non possono essere facilmente idempotenti, ad esempio la creazione di una risorsa, ed è necessario evitare duplicazioni non necessarie. Per questi casi d'uso, il messaggio di richiesta deve contengono un ID univoco, ad esempio un UUID, che il server utilizzerà per rilevare duplicati e fare in modo che la richiesta venga elaborata una sola volta.

// A unique request ID for server to detect duplicated requests.
// This field **should** be named as `request_id`.
string request_id = ...;

Se viene rilevata una richiesta duplicata, il server deve restituire la risposta. per la richiesta andata a buon fine, perché molto probabilmente il client non ricevono la risposta precedente.

Valore predefinito dell'enum

Ogni definizione enum deve iniziare con una voce con valore 0, che dovrà utilizzato quando un valore enum non è specificato in modo esplicito. Le API devono documentare come Vengono gestiti i valori 0.

Il valore enum 0 deve essere denominato ENUM_TYPE_UNSPECIFIED. Se esiste un comportamento predefinito comune, deve essere utilizzato quando non viene specificato esplicitamente un valore dell'enum. Se non esiste un comportamento predefinito comune, 0 il valore deve essere rifiutato con l'errore INVALID_ARGUMENT se utilizzato.

enum Isolation {
  // Not specified.
  ISOLATION_UNSPECIFIED = 0;
  // Reads from a snapshot. Collisions occur if all reads and writes cannot be
  // logically serialized with concurrent transactions.
  SERIALIZABLE = 1;
  // Reads from a snapshot. Collisions occur if concurrent transactions write
  // to the same rows.
  SNAPSHOT = 2;
  ...
}

// When unspecified, the server will use an isolation level of SNAPSHOT or
// better.
Isolation level = 1;

Per il valore 0 può essere utilizzato un nome idiomatico. Ad esempio, google.rpc.Code.OK è il modo idiomatico per specificare l'assenza di un codice di errore. In questo caso, OK è semanticamente equivalente a UNSPECIFIED nel contesto del tipo enum.

Nei casi in cui esiste un valore predefinito intrinsecamente sensibile e sicuro, quel valore può essere utilizzato per lo "0" valore. Ad esempio, BASIC è lo "0" nel parametro Enum in Visualizzazione risorsa.

Sintassi della grammatica

Nei progetti di API, è spesso necessario definire grammatiche semplici per alcuni formati di dati, ad esempio l'input di testo accettabile. Per fornire un un'esperienza di sviluppo coerente tra le API e ridurre la curva di apprendimento, I progettisti di API devono utilizzare la seguente variante di Extended Backus-Naur Sintassi del modulo (EBNF) per definire queste grammaticali:

Production  = name "=" [ Expression ] ";" ;
Expression  = Alternative { "|" Alternative } ;
Alternative = Term { Term } ;
Term        = name | TOKEN | Group | Option | Repetition ;
Group       = "(" Expression ")" ;
Option      = "[" Expression "]" ;
Repetition  = "{" Expression "}" ;

Tipi di numeri interi

Nei progetti API, i tipi di numeri interi non firmati come uint32 e fixed32 non devono essere utilizzati perché alcuni sistemi e linguaggi di programmazione importanti non li supportano bene, ad esempio Java, JavaScript e OpenAPI. Inoltre, è più probabile che causino errori di overflow. Un altro problema è che API diverse usano probabilmente tipi non firmati e non corrispondenti per la stessa cosa.

Quando vengono utilizzati tipi di numeri interi firmati per elementi in cui i valori negativi non sono significative, come dimensioni o timeout, il valore -1 (e solo -1) può da utilizzare per indicare un significato speciale, come la fine del file (EOF), l'infinita timeout, limite di quota illimitato o età sconosciuta. Questi utilizzi devono essere chiaramente documentati per evitare confusione. I produttori di API devono anche documentare il comportamento del valore predefinito implicito 0 se non è molto evidente.

Risposta parziale

A volte un client API ha bisogno solo di un sottoinsieme specifico di dati nel messaggio di risposta. Per supportare questi casi d'uso, alcune piattaforme API forniscono il supporto nativo per le risposte parziali. La piattaforma API di Google lo supporta tramite la maschera del campo di risposta.

Per qualsiasi chiamata all'API REST, è presente un parametro di query di sistema implicito $fields, che è la rappresentazione JSON di un valore google.protobuf.FieldMask. La il messaggio di risposta verrà filtrato in base al $fields prima di essere reinviato a il cliente. Questa logica viene gestita automaticamente dall'API per tutti i metodi dell'API Piattaforma.

GET https://library.googleapis.com/v1/shelves?$fields=shelves.name
GET https://library.googleapis.com/v1/shelves/123?$fields=name

Vista risorse

Per ridurre il traffico di rete, a volte è utile consentire al client di limitare le parti della risorsa che il server deve restituire nelle sue risposte, restituendo una visualizzazione della risorsa anziché la rappresentazione completa della risorsa. Il supporto della visualizzazione della risorsa in un'API viene implementato aggiungendo un parametro alla richiesta del metodo che consente al client di specificare la visualizzazione della risorsa che vuole ricevere nella risposta.

Il parametro:

  • deve essere di tipo enum
  • deve essere denominato view

Ogni valore dell'enumerazione definisce quali parti della risorsa (quali campi) verranno restituite nella risposta del server. Che cosa viene esattamente visualizzato per ogni valore view è definito dall'implementazione e deve essere specificato nella documentazione dell'API.

package google.example.library.v1;

service Library {
  rpc ListBooks(ListBooksRequest) returns (ListBooksResponse) {
    option (google.api.http) = {
      get: "/v1/{name=shelves/*}/books"
    }
  };
}

enum BookView {
  // Not specified, equivalent to BASIC.
  BOOK_VIEW_UNSPECIFIED = 0;

  // Server responses only include author, title, ISBN and unique book ID.
  // The default value.
  BASIC = 1;

  // Full representation of the book is returned in server responses,
  // including contents of the book.
  FULL = 2;
}

message ListBooksRequest {
  string name = 1;

  // Specifies which parts of the book resource should be returned
  // in the response.
  BookView view = 2;
}

Questo costrutto verrà mappato a URL come:

GET https://library.googleapis.com/v1/shelves/shelf1/books?view=BASIC

Per scoprire di più sulla definizione di metodi, richieste e risposte, consulta il capitolo Metodi standard di questa Guida al design.

ETag

Un ETag è un identificatore opaco che consente a un client di effettuare richieste condizionali. Per supportare gli ETag, un'API deve includere un campo di stringa etag nella definizione della risorsa e la relativa semantica deve corrispondere all'utilizzo comune dell'ETag. Normalmente, etag contiene l'impronta della risorsa calcolata server web. Consulta Wikipedia e Per ulteriori dettagli, consulta la pagina RFC 7232.

Gli ETag possono essere convalidati in modo forte o debole, mentre gli ETag con convalida debole hanno il prefisso W/. In questo contesto, la convalida rigorosa indica che due risorse con lo stesso ETag hanno contenuti identici byte per byte e campi aggiuntivi identici (ad es. Content-Type). Ciò significa che le API Gli ETag consentono di memorizzare nella cache risposte parziali da assemblare in un secondo momento.

Al contrario, le risorse che hanno lo stesso valore ETag con convalida debole indicano che le rappresentazioni sono semanticamente equivalenti, ma non necessariamente identiche byte per byte e, pertanto, non sono adatte alla memorizzazione nella cache della risposta delle richieste con intervallo di byte.

Ad esempio:

// This is a strong ETag, including the quotes.
"1a2f3e4d5b6c7c"
// This is a weak ETag, including the prefix and quotes.
W/"1a2b3c4d5ef"

È importante capire che le virgolette fanno effettivamente parte del valore ETag e devono essere presenti per essere conformi allo standard RFC 7232. Ciò significa che le rappresentazioni JSON degli ETag finiscono per sfuggire alle virgolette. Ad esempio, gli ETag verranno rappresentati nei corpi delle risorse JSON come:

// Strong
{ "etag": "\"1a2f3e4d5b6c7c\"", "name": "...", ... }
// Weak
{ "etag": "W/\"1a2b3c4d5ef\"", "name": "...", ... }

Riepilogo dei caratteri consentiti negli ETag:

  • Solo caratteri ASCII stampabili
    • Caratteri non ASCII consentiti dalla RFC 7232, ma meno adatti agli sviluppatori
  • Nessuno spazio
  • Nessuna virgola doppia tranne che nelle posizioni mostrate sopra
  • Evita le barre verticali come consigliato da RFC 7232 per evitare confusione sull'escapismo

Campi di output

Le API potrebbero voler distinguere tra i campi forniti dal client come input e i campi restituiti dal server solo in output in una determinata risorsa. Per i soli campi di output, l'attributo del campo deve essere annotato.

Tieni presente che se i campi di sola uscita sono impostati nella richiesta o inclusi in un google.protobuf.FieldMask, il server deve accettare la richiesta senza errori. Il server deve ignorare la presenza di campi di solo output e qualsiasi un'indicazione. Il motivo di questo consiglio è che i clienti spesso riutilizzare le risorse restituite dal server come ulteriore input di richiesta, ad esempio a L'elemento Book recuperato verrà riutilizzato in un secondo momento in un metodo UPDATE. Se solo campi di output vengono convalidati, ciò comporta del lavoro aggiuntivo da parte del cliente solo campi di output.

import "google/api/field_behavior.proto";

message Book {
  string name = 1;
  Timestamp create_time = 2 [(google.api.field_behavior) = OUTPUT_ONLY];
}

Risorse Singleton

Una risorsa singleton può essere utilizzata quando si utilizza una sola istanza di una risorsa esiste all'interno della risorsa padre (o all'interno dell'API, se non ha un padre).

I metodi Create e Delete standard devono essere omessi per le risorse singleton. Il singleton viene creato o eliminato implicitamente quando viene creato o eliminato il relativo elemento principale (ed esiste implicitamente se non ha un elemento principale). Per accedere alla risorsa è necessario utilizzare i metodi standard Get e Update, nonché eventuali metodi personalizzati appropriati per il tuo caso d'uso.

Ad esempio, un'API con User risorse potrebbe esporre le impostazioni per utente come Settings singleton.

rpc GetSettings(GetSettingsRequest) returns (Settings) {
  option (google.api.http) = {
    get: "/v1/{name=users/*/settings}"
  };
}

rpc UpdateSettings(UpdateSettingsRequest) returns (Settings) {
  option (google.api.http) = {
    patch: "/v1/{settings.name=users/*/settings}"
    body: "settings"
  };
}

[...]

message Settings {
  string name = 1;
  // Settings fields omitted.
}

message GetSettingsRequest {
  string name = 1;
}

message UpdateSettingsRequest {
  Settings settings = 1;
  // Field mask to support partial updates.
  FieldMask update_mask = 2;
}

Mezza chiusura streaming

Per qualsiasi API bidirezionale o di flusso client, il server deve fare affidamento su la mezza chiusura avviata dal client, come fornita dal sistema RPC, per completare per lo stream lato client. Non è necessario definire un messaggio di completamento esplicito.

Qualsiasi informazione che il cliente deve inviare prima della chiusura di metà deve come parte del messaggio di richiesta.

Nomi a livello di dominio

Un nome con ambito di dominio è un nome di entità che è preceduto da un nome di dominio DNS a per evitare i conflitti di nomi. Si tratta di un pattern di progettazione utile quando diverse organizzazioni definiscono i nomi delle entità in modo decentralizzato. La sintassi è simile a un URI senza uno schema.

I nomi con ambito di dominio sono ampiamente utilizzati tra le API di Google e le API Kubernetes, come come:

  • La rappresentazione del tipo Any di Protobuf: type.googleapis.com/google.protobuf.Any
  • Tipi di metriche Stackdriver: compute.googleapis.com/instance/cpu/utilization
  • Chiavi di etichetta: cloud.googleapis.com/location
  • Versioni dell'API Kubernetes: networking.k8s.io/v1
  • Il campo kind nell'estensione OpenAPI x-kubernetes-group-version-kind.

Bool, enum e stringa

Quando si progetta un metodo API, è molto comune offrire un insieme di scelte per una funzionalità specifica, come l'attivazione del tracciamento o la disattivazione della memorizzazione nella cache. Il modo più comune per farlo è introdurre un campo della richiesta di tipo bool, enum o string. Non è sempre chiaro quale sia il tipo giusto da usare caso d'uso specifico. La scelta consigliata è la seguente:

  • Usare il tipo bool se vogliamo avere un design fisso e intenzionalmente non vuoi estendere la funzionalità. Ad esempio, bool enable_tracing o bool enable_pretty_print.

  • Utilizzare un tipo enum se vogliamo avere un design flessibile, ma non prevediamo che il design cambierà spesso. La regola empirica è la definizione di enum cambierà solo una volta all'anno o con una frequenza minore. Ad esempio, enum TlsVersion o enum HttpVersion.

  • Viene usato il tipo string se abbiamo un design aperto o il design può essere vengono modificate spesso da uno standard esterno. I valori supportati devono essere chiaramente documentati. Ad esempio:

Conservazione dei dati

Quando si progetta un servizio API, la conservazione dei dati è un aspetto fondamentale del servizio l'affidabilità. È comune che i dati utente vengano eliminati per errore da bug del software o da errori umani. Senza la conservazione dei dati e la funzionalità di annullamento dell'eliminazione corrispondente, un semplice errore può avere un impatto catastrofico sull'attività.

In generale, consigliamo le seguenti norme sulla conservazione dei dati per i servizi API:

  • Per i metadati utente, le impostazioni utente e altre informazioni importanti, deve essere prevista la conservazione dei dati per 30 giorni. Ad esempio, monitoraggio delle metriche, metadati di progetto e definizioni di servizi.

  • Per contenuti utente con volumi elevati, i dati dovrebbero essere conservati per 7 giorni. Ad esempio, blob binari e tabelle di database.

  • Per lo stato temporaneo o l'archiviazione costosa, dovrebbero essere presenti dati di 1 giorno se possibile. ad esempio istanze memcache e server Redis.

Durante il periodo di conservazione dei dati, i dati possono essere annullati senza perdita di dati. Se è costoso offrire la conservazione dei dati gratuitamente, un servizio può offrire la conservazione dei dati come opzione a pagamento.

Payload di grandi dimensioni

Le API in rete spesso dipendono da più livelli di rete per il loro percorso dati. La maggior parte dei livelli di rete ha limiti rigidi per le dimensioni di richieste e risposte. 32 MB è un limite di uso comune in molti sistemi.

Quando si progetta un metodo API che gestisce payload di dimensioni superiori a 10 MB, scegliere con cura la giusta strategia per l'usabilità e la crescita futura. Per Google API, consigliamo di usare sia lo streaming che il caricamento/download dei contenuti multimediali di grandi dimensioni. Con i flussi di dati, il server gestisce in modo incrementale i dati di grandi dimensioni in modo sincrono, come l'API Cloud Spanner. Con i media, i grandi flussi di dati attraverso un sistema di archiviazione di grandi dimensioni, come Google Cloud Storage, e il server possono gestire i dati in modo asincrono, come l'API Google Drive.

Campi originari facoltativi

Protocol Buffers v3 (proto3) supporta i campi primitivi optional, che sono semanticamente equivalenti ai tipi nullable in molti linguaggi di programmazione. Possono essere utilizzati per distinguere i valori vuoti dai valori non impostati.

In pratica, è difficile per gli sviluppatori gestire correttamente i campi facoltativi. La maggior parte delle librerie client HTTP JSON, tra cui le librerie client dell'API di Google, non è in grado di distinguere proto3 int32, google.protobuf.Int32Value e optional int32. Se un design alternativo è altrettanto chiaro e non richiede un elemento primitivo facoltativo, preferisci quello. Se non lo utilizzi, complessità o ambiguità, quindi utilizza campi primitivi facoltativi. I tipi di wrapper non devono essere utilizzati in futuro. In generale, i progettisti delle API dovrebbero usare tipi primitivi semplici, come int32, per semplicità e coerenza.