Wie verwalten Sie die zugrunde liegende Codebasis für eine versionierte API?

104

Ich habe mich über Versionsstrategien für ReST-APIs informiert, und keine davon scheint sich mit der Verwaltung der zugrunde liegenden Codebasis zu befassen.

Nehmen wir an, wir nehmen eine Reihe von Änderungen an einer API vor, z. B. indem wir unsere Kundenressource so ändern, dass sie separate forenameund surnameFelder anstelle eines einzelnen nameFelds zurückgibt . (In diesem Beispiel verwende ich die URL-Versionierungslösung, da die Konzepte leicht zu verstehen sind. Die Frage gilt jedoch auch für die Aushandlung von Inhalten oder benutzerdefinierte HTTP-Header.)

Wir haben jetzt einen Endpunkt bei http://api.mycompany.com/v1/customers/{id}und einen anderen inkompatiblen Endpunkt bei http://api.mycompany.com/v2/customers/{id}. Wir veröffentlichen immer noch Bugfixes und Sicherheitsupdates für die v1-API, aber die Entwicklung neuer Funktionen konzentriert sich jetzt alle auf v2. Wie schreiben, testen und implementieren wir Änderungen auf unserem API-Server? Ich kann mindestens zwei Lösungen sehen:

  • Verwenden Sie einen Versionsverwaltungszweig / ein Tag für die v1-Codebasis. v1 und v2 werden unabhängig voneinander entwickelt und bereitgestellt, wobei bei Bedarf Revisionskontrollzusammenführungen verwendet werden, um denselben Bugfix auf beide Versionen anzuwenden - ähnlich wie Sie Codebasen für native Apps verwalten würden, wenn Sie eine neue Hauptversion entwickeln und gleichzeitig die vorherige Version unterstützen.

  • Machen Sie die Codebasis selbst auf die API-Versionen aufmerksam, sodass Sie eine einzige Codebasis erhalten, die sowohl die v1-Kundenrepräsentation als auch die v2-Kundenrepräsentation enthält. Behandeln Sie die Versionierung als Teil Ihrer Lösungsarchitektur anstelle eines Bereitstellungsproblems. Verwenden Sie möglicherweise eine Kombination aus Namespaces und Routing, um sicherzustellen, dass Anforderungen von der richtigen Version verarbeitet werden.

Der offensichtliche Vorteil des Zweigstellenmodells besteht darin, dass es trivial ist, alte API-Versionen zu löschen - beenden Sie einfach die Bereitstellung des entsprechenden Zweigs / Tags -, aber wenn Sie mehrere Versionen ausführen, kann dies zu einer wirklich komplizierten Zweigstruktur und Bereitstellungspipeline führen. Das "Unified Codebase" -Modell vermeidet dieses Problem, aber (glaube ich?) Würde es viel schwieriger machen, veraltete Ressourcen und Endpunkte aus der Codebasis zu entfernen, wenn sie nicht mehr benötigt werden. Ich weiß, dass dies wahrscheinlich subjektiv ist, da es wahrscheinlich keine einfache richtige Antwort gibt, aber ich bin gespannt, wie Organisationen, die komplexe APIs über mehrere Versionen hinweg verwalten, dieses Problem lösen.

Dylan Beattie
quelle
41
Vielen Dank für diese Frage! Ich kann nicht glauben, dass mehr Leute diese Frage nicht beantworten !! Ich habe es satt, dass jeder eine Meinung dazu hat, wie Versionen in ein System gelangen, aber niemand scheint das wirklich schwierige Problem anzugehen, Versionen an den entsprechenden Code zu senden. Inzwischen sollte es mindestens eine Reihe akzeptierter "Muster" oder "Lösungen" für dieses scheinbar häufige Problem geben. Es gibt eine wahnsinnige Anzahl von Fragen zu SO bezüglich "API-Versionierung". Die Entscheidung, wie Versionen akzeptiert werden sollen, ist FRIKKIN SIMPLE (relativ)! Es ist SCHWER, es in der Codebasis zu handhaben, sobald es eingeht.
Arijeet

Antworten:

45

Ich habe beide Strategien verwendet, die Sie erwähnen. Von diesen beiden favorisiere ich den zweiten Ansatz, der einfacher ist, in Anwendungsfällen, die ihn unterstützen. Das heißt, wenn die Versionsanforderungen einfach sind, wählen Sie ein einfacheres Software-Design:

  • Eine geringe Anzahl von Änderungen, Änderungen mit geringer Komplexität oder ein Zeitplan für Änderungen mit geringer Frequenz
  • Änderungen, die weitgehend orthogonal zum Rest der Codebasis sind: Die öffentliche API kann friedlich mit dem Rest des Stapels existieren, ohne dass eine "übermäßige" Verzweigung (für jede Definition des Begriffs, den Sie übernehmen möchten) im Code erforderlich ist

Ich fand es nicht allzu schwierig, veraltete Versionen mit diesem Modell zu entfernen:

  • Eine gute Testabdeckung bedeutete, dass das Herausreißen einer ausgemusterten API und des zugehörigen Sicherungscodes keine (gut, minimalen) Regressionen sicherstellte
  • Eine gute Namensstrategie (API-versionierte Paketnamen oder etwas hässlichere API-Versionen in Methodennamen) machte es einfach, den relevanten Code zu finden
  • Querschnittsthemen sind schwieriger; Änderungen an Kern-Backend-Systemen zur Unterstützung mehrerer APIs müssen sehr sorgfältig abgewogen werden. Irgendwann überwiegen die Kosten für die Versionierung des Backends (siehe Kommentar zu "übermäßig" oben) den Vorteil einer einzelnen Codebasis.

Der erste Ansatz ist unter dem Gesichtspunkt der Reduzierung von Konflikten zwischen gleichzeitig vorhandenen Versionen sicherlich einfacher, aber der Aufwand für die Wartung separater Systeme überwog tendenziell den Vorteil der Reduzierung von Versionskonflikten. Trotzdem war es kinderleicht, einen neuen öffentlichen API-Stack zu erstellen und mit der Iteration in einem separaten API-Zweig zu beginnen. Natürlich setzte der Generationsverlust fast sofort ein und die Zweige verwandelten sich in ein Durcheinander von Zusammenschlüssen, Zusammenführen von Konfliktlösungen und anderem solchen Spaß.

Ein dritter Ansatz betrifft die Architekturebene: Nehmen Sie eine Variante des Fassadenmusters an und abstrahieren Sie Ihre APIs in öffentlich zugängliche, versionierte Ebenen, die mit der entsprechenden Fassadeninstanz kommunizieren, die wiederum über ihre eigenen APIs mit dem Backend kommuniziert. Ihre Fassade (ich habe in meinem vorherigen Projekt einen Adapter verwendet) wird zu einem eigenen Paket, das in sich geschlossen und testbar ist und es Ihnen ermöglicht, Frontend-APIs unabhängig vom Backend und voneinander zu migrieren.

Dies funktioniert, wenn Ihre API-Versionen dazu neigen, dieselben Arten von Ressourcen verfügbar zu machen, jedoch mit unterschiedlichen strukturellen Darstellungen, wie in Ihrem Beispiel für vollständigen Namen / Vor- / Nachnamen. Es wird etwas schwieriger, wenn sie sich auf verschiedene Backend-Berechnungen verlassen, wie in "Mein Backend-Service hat falsch berechnete Zinseszinsen zurückgegeben, die in der öffentlichen API v1 verfügbar gemacht wurden. Unsere Kunden haben dieses falsche Verhalten bereits gepatcht. Daher kann ich das nicht aktualisieren." Berechnung im Backend und lassen Sie es bis v2 anwenden. Deshalb müssen wir jetzt unseren Zinsberechnungscode geben. " Glücklicherweise sind diese eher selten: In der Praxis bevorzugen Verbraucher von RESTful-APIs genaue Ressourcendarstellungen gegenüber der Abwärtskompatibilität von Fehler zu Fehler, selbst bei nicht bahnbrechenden Änderungen an einer theoretisch idempotenten GETRessource.

Ich werde interessiert sein, Ihre eventuelle Entscheidung zu hören.

Palpatim
quelle
5
Nur neugierig, duplizieren Sie im Quellcode Modelle zwischen v0 und v1, die sich nicht geändert haben? Oder haben Sie v1 einige v0 Modelle verwendet? Für mich wäre ich verwirrt, wenn ich sehen würde, dass v1 für einige Felder v0-Modelle verwendet. Auf der anderen Seite würde dies das Aufblähen des Codes verringern. Müssen wir für die Verarbeitung mehrerer Versionen nur doppelten Code für Modelle akzeptieren und damit leben, die sich nie geändert haben?
EdgeCaseBerg
1
Ich erinnere mich, dass unsere Quellcode-versionierten Modelle unabhängig von der API selbst sind, sodass beispielsweise API v1 möglicherweise Modell V1 und API v2 möglicherweise auch Modell V1 verwenden. Grundsätzlich enthielt das interne Abhängigkeitsdiagramm für die öffentliche API sowohl exponierten API-Code als auch Backend-Fulfillment-Code wie Server- und Modellcode. Für mehrere Versionen ist die einzige Strategie, die ich jemals verwendet habe, die Duplizierung des gesamten Stapels - ein hybrider Ansatz (Modul A wird dupliziert, Modul B wird versioniert ...) scheint sehr verwirrend. YMMV natürlich. :)
Palpatim
2
Ich bin mir nicht sicher, ob ich den Vorschlägen für den dritten Ansatz folge. Gibt es öffentliche Beispiele für Code, der so aufgebaut ist?
Ehtesh Choudhury
13

Für mich ist der zweite Ansatz besser. Ich habe es für die SOAP-Webdienste verwendet und plane, es auch für REST zu verwenden.

Während Sie schreiben, sollte die Codebasis versionfähig sein, aber eine Kompatibilitätsebene kann als separate Ebene verwendet werden. In Ihrem Beispiel kann die Codebasis eine Ressourcendarstellung (JSON oder XML) mit Vor- und Nachnamen erstellen, die Kompatibilitätsebene ändert sie jedoch so, dass stattdessen nur der Name angezeigt wird.

Die Codebasis sollte nur die neueste Version implementieren, sagen wir v3. Die Kompatibilitätsschicht sollte die Anforderungen und Antworten zwischen der neuesten Version v3 und den unterstützten Versionen, z. B. v1 und v2, konvertieren. Die Kompatibilitätsschicht kann für jede unterstützte Version separate Adapter haben, die als Kette verbunden werden können.

Beispielsweise:

Client v1 Anfrage: v1 an v2 anpassen ---> v2 an v3 anpassen ----> Codebasis

Client v2-Anforderung: v1 an v2 anpassen (überspringen) ---> v2 an v3 anpassen ----> Codebasis

Für die Antwort arbeiten die Adapter einfach in die entgegengesetzte Richtung. Wenn Sie Java EE verwenden, können Sie beispielsweise die Servlet-Filterkette als Adapterkette verwenden.

Das Entfernen einer Version ist einfach. Löschen Sie den entsprechenden Adapter und den Testcode.

S.Stavreva
quelle
Es ist schwierig, die Kompatibilität zu gewährleisten, wenn sich die gesamte zugrunde liegende Codebasis geändert hat. Viel sicherer, die alte Codebasis für Bugfix-Releases beizubehalten.
Marcelo Cantos
5

Die Verzweigung scheint mir viel besser zu sein, und ich habe diesen Ansatz in meinem Fall verwendet.

Ja, wie Sie bereits erwähnt haben - das Zurückportieren von Fehlerkorrekturen erfordert einige Anstrengungen, aber gleichzeitig erfordert die Unterstützung mehrerer Versionen unter einer Quellbasis (mit Routing und allen anderen Dingen) Sie, wenn nicht weniger, aber mindestens denselben Aufwand, um das System mehr zu machen kompliziert und monströs mit verschiedenen Zweigen der Logik im Inneren (irgendwann werden Sie definitiv zu einem großen case()Hinweis auf Versionsmodule kommen, deren Code dupliziert wurde oder noch schlimmer ist if(version == 2) then...). Vergessen Sie auch nicht, dass Sie für Regressionszwecke die Tests immer noch verzweigt halten müssen.

In Bezug auf die Versionsrichtlinien: Ich würde maximal -2 Versionen von der aktuellen Version beibehalten und die Unterstützung für alte Versionen ablehnen - dies würde den Benutzern eine gewisse Motivation zum Umzug geben.

edmarisov
quelle
Ich denke gerade darüber nach, in einer einzigen Codebasis zu testen. Sie haben erwähnt, dass Tests immer verzweigt werden müssen, aber ich denke, dass alle Tests für v1, v2, v3 usw. auch in derselben Lösung ausgeführt werden können und alle zur gleichen Zeit ausgeführt werden. Ich denke , die Tests mit den Attributen der Dekoration , die angeben , welche Versionen unterstützen sie: zB [Version(From="v1", To="v2")], [Version(From="v2", To="v3")], [Version(From="v1")] // All versions es gerade jetzt zu erforschen, jemals jemand gehört es?
Lee Gunn
1
Nun, nach 3 Jahren habe ich erfahren, dass es keine genaue Antwort auf die ursprüngliche Frage gibt: D. Es ist sehr projektabhängig. Wenn Sie es sich leisten können, die API einzufrieren und nur zu warten (z. B. Bugfixes), würde ich den zugehörigen Code (API-bezogene Geschäftslogik + Tests + Restendpunkt) weiterhin verzweigen / trennen und alle gemeinsam genutzten Inhalte in einer separaten Bibliothek (mit eigenen Tests) haben ). Wenn V1 für einige Zeit mit V2 koexistieren wird und die Arbeit an Features noch nicht abgeschlossen ist, würde ich sie zusammenhalten und auch testen (V1, V2 usw. abdecken und entsprechend benennen).
Edmarisov
1
Vielen Dank. Ja, es scheint ein ziemlich einfühlsamer Raum zu sein. Ich werde zuerst den One-Solution-Ansatz ausprobieren und sehen, wie es geht.
Lee Gunn
0

Normalerweise ist die Einführung einer Hauptversion der API, die Sie in die Situation führt, dass mehrere Versionen verwaltet werden müssen, ein Ereignis, das nicht sehr häufig auftritt (oder nicht auftreten sollte). Es kann jedoch nicht vollständig vermieden werden. Ich denke, es ist insgesamt eine sichere Annahme, dass eine einmal eingeführte Hauptversion für einen relativ langen Zeitraum die neueste Version bleiben würde. Auf dieser Grundlage würde ich es vorziehen, den Code auf Kosten der Duplizierung zu vereinfachen, da ich dadurch sicherer bin, dass ich die vorherige Version nicht beschädige, wenn ich Änderungen an der neuesten Version vornehme.

user1537847
quelle