Come gestisci la base di codice sottostante per un'API con versione?


105

Ho letto le strategie di controllo delle versioni per le API ReST e qualcosa che nessuno di loro sembra affrontare è il modo in cui gestisci la base di codice sottostante.

Supponiamo che stiamo apportando una serie di modifiche sostanziali a un'API, ad esempio, cambiando la nostra risorsa Cliente in modo che restituisca campi forenamee separati surnameinvece di un singolo namecampo. (Per questo esempio, userò la soluzione di controllo delle versioni URL poiché è facile comprendere i concetti coinvolti, ma la domanda è ugualmente applicabile alla negoziazione del contenuto o alle intestazioni HTTP personalizzate)

Ora abbiamo un endpoint in http://api.mycompany.com/v1/customers/{id}e un altro endpoint incompatibile in http://api.mycompany.com/v2/customers/{id}. Stiamo ancora rilasciando correzioni di bug e aggiornamenti di sicurezza per l'API v1, ma lo sviluppo di nuove funzionalità è ora incentrato sulla v2. Come scriviamo, testiamo e distribuiamo le modifiche al nostro server API? Posso vedere almeno due soluzioni:

  • Utilizzare un tag / ramo di controllo del codice sorgente per la base di codice v1. v1 e v2 sono sviluppate e distribuite in modo indipendente, con unioni di controllo di revisione utilizzate secondo necessità per applicare la stessa correzione di bug a entrambe le versioni, in modo simile a come gestireste le basi di codice per le app native quando sviluppate una nuova versione principale pur supportando la versione precedente.

  • Rendi la codebase stessa a conoscenza delle versioni API, in modo da ottenere una singola codebase che include sia la rappresentazione del cliente v1 che la rappresentazione del cliente v2. Considera il controllo delle versioni come parte dell'architettura della tua soluzione invece che come un problema di distribuzione, probabilmente utilizzando una combinazione di spazi dei nomi e instradamento per assicurarti che le richieste siano gestite dalla versione corretta.

L'ovvio vantaggio del modello di filiale è che è banale eliminare le vecchie versioni dell'API - basta smettere di distribuire il ramo / tag appropriato - ma se si eseguono più versioni, si potrebbe finire con una struttura di ramo e una pipeline di distribuzione davvero contorta. Il modello "base di codice unificata" evita questo problema, ma (credo?) Renderebbe molto più difficile rimuovere le risorse e gli endpoint deprecati dalla base di codice quando non sono più necessari. So che questo è probabilmente soggettivo poiché è improbabile che ci sia una risposta semplice e corretta, ma sono curioso di capire come le organizzazioni che mantengono API complesse su più versioni risolvono questo problema.


41
Grazie per aver posto questa domanda! NON POSSO credere che più persone non stiano rispondendo a questa domanda !! Sono stufo e stanco che tutti abbiano un'opinione su come le versioni entrano in un sistema, ma nessuno sembra affrontare il vero e difficile problema di inviare le versioni al loro codice appropriato. A questo punto dovrebbe esserci almeno una serie di "modelli" o "soluzioni" accettati a questo problema apparentemente comune. C'è un numero folle di domande su SO riguardo al "controllo delle versioni delle API". Decidere come accettare le versioni è FRIKKIN SEMPLICE (relativamente)! Gestirlo nella base di codice una volta inserito è DIFFICILE!
arijeet

Risposte:


45

Ho usato entrambe le strategie che hai menzionato. Di questi due, preferisco il secondo approccio, essendo più semplice, nei casi d'uso che lo supportano. Cioè, se le esigenze di controllo delle versioni sono semplici, allora scegli un design software più semplice:

  • Un numero ridotto di modifiche, modifiche a bassa complessità o pianificazione delle modifiche a bassa frequenza
  • Modifiche che sono in gran parte ortogonali al resto del codebase: l'API pubblica può esistere pacificamente con il resto dello stack senza richiedere ramificazioni "eccessive" (per qualunque definizione di quel termine si sceglie di adottare) nel codice

Non ho trovato eccessivamente difficile rimuovere le versioni deprecate utilizzando questo modello:

  • Una buona copertura dei test significava che l'estrazione di un'API ritirata e del codice di supporto associato non assicurava regressioni (beh, minime)
  • Una buona strategia di denominazione (nomi di pacchetti con versione API o versioni API un po 'più brutte nei nomi di metodo) ha reso facile individuare il codice pertinente
  • Le preoccupazioni trasversali sono più difficili; le modifiche ai sistemi di backend core per supportare più API devono essere valutate con molta attenzione. Ad un certo punto, il costo del backend di versioning (vedere il commento su "eccessivo" sopra) supera il vantaggio di una singola base di codice.

Il primo approccio è certamente più semplice dal punto di vista della riduzione del conflitto tra versioni coesistenti, ma il sovraccarico di mantenere sistemi separati tendeva a superare il vantaggio di ridurre il conflitto di versioni. Detto questo, era estremamente semplice creare un nuovo stack API pubblico e iniziare a iterare su un ramo API separato. Ovviamente, la perdita generazionale si è verificata quasi immediatamente ei rami si sono trasformati in un pasticcio di fusioni, risoluzioni di conflitti di fusione e altri divertimenti simili.

Un terzo approccio è a livello di architettura: adotta una variante del modello Facade e astrarre le tue API in livelli rivolti al pubblico e con versione che dialoga con l'istanza Facade appropriata, che a sua volta dialoga con il back-end tramite il proprio set di API. Your Facade (ho utilizzato un adattatore nel mio progetto precedente) diventa un pacchetto a sé stante, autonomo e testabile, e consente di migrare le API frontend indipendentemente dal backend e l'una dall'altra.

Questo funzionerà se le versioni dell'API tendono a esporre gli stessi tipi di risorse, ma con rappresentazioni strutturali diverse, come nell'esempio di nome completo / nome / cognome. Diventa leggermente più difficile se iniziano a fare affidamento su diversi calcoli di backend, come in "Il mio servizio di backend ha restituito un interesse composto calcolato in modo errato che è stato esposto nell'API pubblica v1. I nostri clienti hanno già corretto questo comportamento errato. Pertanto, non posso aggiornarlo calcolo nel backend e applicarlo fino alla v2. Quindi ora dobbiamo fare il fork del nostro codice di calcolo degli interessi. " Fortunatamente, questi tendono ad essere rari: in pratica, i consumatori di API RESTful preferiscono rappresentazioni accurate delle risorse rispetto alla compatibilità con le versioni precedenti bug per bug, anche tra modifiche continue su una GETrisorsa ted teoricamente idempotente .

Mi interesserà conoscere la tua eventuale decisione.


5
Solo curioso, nel codice sorgente, duplichi modelli tra v0 e v1 che non sono cambiati? O hai v1 usa alcuni modelli v0? Per me, sarei confuso se vedessi v1 utilizzando modelli v0 per alcuni campi. Ma d'altra parte, ridurrebbe il blocco del codice. Per gestire più versioni dobbiamo solo accettare e convivere con codice duplicato per modelli che non sono mai cambiati?
EdgeCaseBerg

1
Mi ricordo che i nostri modelli con versione del codice sorgente indipendentemente dall'API stessa, quindi, ad esempio, l'API v1 potrebbe utilizzare il modello V1 e l'API v2 potrebbe anche utilizzare il modello V1. Fondamentalmente, il grafico delle dipendenze interne per l'API pubblica includeva sia il codice API esposto, sia il codice di "adempimento" del backend come il codice del server e del modello. Per più versioni, l'unica strategia che ho mai usato è la duplicazione dell'intero stack: un approccio ibrido (il modulo A è duplicato, il modulo B è versionato ...) sembra molto confuso. YMMV ovviamente. :)
Palpatim

2
Non sono sicuro di seguire quanto suggerito per il terzo approccio. Esistono esempi pubblici di codice strutturato in questo modo?
Ehtesh Choudhury

13

Per me il secondo approccio è migliore. L'ho usato per i servizi web SOAP e ho intenzione di usarlo anche per REST.

Durante la scrittura, la base di codice dovrebbe riconoscere la versione, ma è possibile utilizzare un livello di compatibilità come livello separato. Nel tuo esempio, la codebase può produrre la rappresentazione della risorsa (JSON o XML) con il nome e il cognome, ma il livello di compatibilità lo cambierà per avere invece solo il nome.

Il codebase dovrebbe implementare solo l'ultima versione, diciamo v3. Il livello di compatibilità dovrebbe convertire le richieste e le risposte tra la versione più recente v3 e le versioni supportate, ad esempio v1 e v2. Il livello di compatibilità può avere adattatori separati per ciascuna versione supportata che possono essere collegati come catena.

Per esempio:

Richiesta client v1: v1 si adatta a v2 ---> v2 si adatta a v3 ----> codebase

Richiesta client v2: v1 si adatta a v2 (salta) ---> v2 si adatta a v3 ----> codebase

Per la risposta gli adattatori funzionano semplicemente nella direzione opposta. Se si utilizza Java EE, è possibile utilizzare la catena di filtri servlet come catena di adattatori, ad esempio.

Rimuovere una versione è facile, elimina l'adattatore corrispondente e il codice di prova.


È difficile garantire la compatibilità se l'intera base di codice sottostante è cambiata. È molto più sicuro conservare la vecchia base di codice per le versioni di correzione di bug.
Marcelo Cantos

5

La ramificazione sembra molto meglio per me e ho usato questo approccio nel mio caso.

Sì, come hai già accennato: le correzioni di bug di backport richiederanno un certo sforzo, ma allo stesso tempo supportare più versioni sotto una base di origine (con il routing e tutte le altre cose) richiederà se non meno, ma almeno lo stesso sforzo, rendendo il sistema più complicato e mostruoso con diversi rami della logica all'interno (ad un certo punto del controllo delle versioni si arriverà sicuramente a un enorme case()puntamento a moduli di versione con codice duplicato o che hanno anche peggio if(version == 2) then...). Inoltre, non dimenticare che per scopi di regressione devi ancora mantenere i test ramificati.

Per quanto riguarda la politica di controllo delle versioni: manterrei al massimo -2 versioni dall'attuale, deprecando il supporto per quelle vecchie - ciò darebbe agli utenti una motivazione per spostarsi.


Al momento sto pensando di testare in una singola base di codice. Hai detto che i test avrebbero sempre bisogno di essere ramificati, ma penso che tutti i test per v1, v2, v3 ecc. Potrebbero anche vivere nella stessa soluzione ed essere eseguiti tutti allo stesso tempo. Sto pensando di decorare i test con gli attributi che specificano quali versioni supportano: ad esempio [Version(From="v1", To="v2")], [Version(From="v2", To="v3")], [Version(From="v1")] // All versions semplicemente esplorando ora, mai sentito nessuno farlo?
Lee Gunn

1
Ebbene, dopo 3 anni ho appreso che non esiste una risposta precisa alla domanda originale: D. È molto dipendente dal progetto. Se puoi permetterti di congelare l'API e mantenerla solo (es. Correzioni di bug), allora diramerei / scollegherei comunque il codice correlato (logica aziendale correlata all'API + test + endpoint di riposo) e avrò tutte le cose condivise in una libreria separata (con i propri test ). Se V1 coesisterà con V2 per un bel po 'di tempo e il lavoro sulle funzionalità è ancora in corso, li terrei insieme e anche i test (coprendo V1, V2 ecc. E denominati di conseguenza).
edmarisov

1
Grazie. Sì, sembra essere uno spazio piuttosto ostinato. Proverò prima l'approccio a una soluzione e vedrò come va.
Lee Gunn

0

Di solito, l'introduzione di una versione principale dell'API che ti porta in una situazione di dover mantenere più versioni è un evento che non si verifica (o non dovrebbe) molto frequentemente. Tuttavia, non può essere evitato completamente. Penso che sia nel complesso un presupposto sicuro che una versione principale, una volta introdotta, rimarrà l'ultima versione per un periodo di tempo relativamente lungo. Sulla base di ciò, preferirei ottenere la semplicità nel codice a scapito della duplicazione in quanto mi dà maggiore sicurezza di non rompere la versione precedente quando introduco modifiche in quella più recente.

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.