Best Practices bei der API-Versionierung

Die API-Versionierung ist ein von Entwicklern und Softwarearchitekten häufig kontrovers diskutiertes Thema. Insbesondere bei Web-APIs gibt es unterschiedliche Ansichten, ob und wie Versionierung zu erfolgen hat. Deswegen lohnt es sich, einmal genauer grundlegende Überlegungen anzustellen.

Know-how  –  3 Kommentare
Best Practices bei der API-Versionierung

Die Kernidee von Continuous Integration ist bekanntlich, dass Softwareentwickler ihre Code-Änderungen kontinuierlich in eine gemeinsame Codebasis integrieren, um Integrationsprobleme zu vermeiden oder sie zumindest zeitnah in kleinen Schritten beheben zu können. Das Update einer Drittanbieterbibliothek oder der Umstieg auf einen neueren Dienst geschieht seltener und kann durchaus zeitintensiv sein. Besonders gemein sind Integrationsprobleme bei einem Bugfix-Release.

Die API einer Bibliothek oder eines Dienstes sollte deswegen möglichst stabil sein. Gerade im Unternehmensumfeld möchte man Änderungen an Legacy-Client-Anwendungen vermeiden. Auch die Betreiber einer öffentlichen Web-API müssen bei Updates gut aufpassen. Wenn beispielsweise Twitter die eigene REST-API ändern würde, wären Millionen von Client-Installationen zu aktualisieren.

API-Verträge und Verhaltenstests

Eine API beschreibt das Verhalten einer Softwarekomponente oder eines Dienstes, sodass sich zwischen ihrem Anbieter und dem Konsument ein Vertrag schließen lässt. Wenn sich beide Seiten an ihn halten, sollte die Integration funktionieren. Weil der Vertrag sich auf die API, nicht aber auf ihre Implementierung bezieht, könnte der API-Anbieter Letztere ändern oder sogar austauschen. Interessant wird die Angelegenheit, falls eine Korrektur oder Erweiterung das extern sichtbare Verhalten ändert.

Um sicherzustellen, dass sich dieses Verhalten einer Softwarekomponente oder eines Dienstes nicht unerwartet ändert, sollte die Einhaltung des API-Vertrags mit Verhaltenstests überprüft werden. Bei der Java-Klassenbibliothek kommt beispielsweise hierfür das Technology Compatibility Kit zum Einsatz. Diese Tests bauen auf zwei Säulen auf:

  1. Kompatibilitätstests der API-Signatur
  2. API-Verhaltenstests

Ganz allgemein sind zwei Softwarekomponenten S1 und S2 zueinander kompatibel, falls sie sich gegeneinander austauschen lassen, ohne dass Clients das bemerken würden. Analog könnte man auch von zwei Diensten (z. B. RESTful HTTP) sprechen.

Im Fall von Java kann man zwischen Code-, binärer und funktionaler Kompatibilität unterscheiden: Code-Kompatibilität bedeutet, dass alle Client-Anwendungen von S1 ohne Anpassung auch mit S2 kompilieren. Binäre Kompatibilität bedeutet, dass sich alle Client-Anwendungen von S1 auch bei S2 dürchführen lassen. Falls sich zusätzlich S2 wie S1 verhält, sind beide funktional kompatibel zueinander.

API-Signaturkompatibilitätstests lassen sich mit einem Tool wie SigTest durchführen. Dieses Kommandozeilenwerkzeug kann sowohl Code- als auch binäre Kompatibilität überprüfen. Es erstellt zunächst eine Datei mit Signaturbeschreibungen für S1 und vergleicht diese dann mit denen von S2.

Die API-Verhaltenstests sind letztlich eine Testsuite bestehend aus Komponenten- oder Integrationstests, die man für S1 erstellt und für S2 wiederverwendet. Die Verhaltenstests schreibt in der Regel der API-Anbieter. Die Clients könnten jedoch die vereinbarten Anforderungen anders interpretieren oder ein Verhalten der API-Implementierung nutzen, das die Tests nicht überprüfen. Dieses "inoffizielle" und implementierungsspezifische Verhalten könnte sich beim nächsten Release ändern. Eine Lösung könnte das sogenannte Client-Driven Contract Testing sein.

Der Ansatz kommt Situationen zugute, in denen es mehrere Clients (API-Konsumenten) mit unterschiedlichen Anforderungen gibt. Diese Situation ist leider der Normalfall. Das Besondere an dem Ansatz ist, dass die Clients bekannt und in der Lage sind, ihre individuellen Anforderungen an die API zu darzulegen. Die Clients schreiben für diesen Zweck Integrationstests, die sie dem API-Anbieter zur Verfügung stellen. Er kann die Tests seiner Clients benutzen, um seine API-Implementierung zu testen. Falls dennoch ein Integrationsproblem mit einem Client auftritt, liegt es höchstwahrscheinlich daran, dass er seine Anforderungen nicht vollständig mit den Tests definiert hat.

Rück- und Vorwärtskompatibilität

Zuvor wurde Kompatibilität als Beziehung zwischen zwei Softwarekomponenten beschrieben. Wenn bei ihnen auch ein historischer Zusammenhang besteht, weil etwa S2 durch Änderung von S1 entstand, spricht man von Rück- und Vorwärtskompatibilität.

Sie lässt sich anhand folgendem Beispiel erklären: Angenommen, es wurde eine Client-Anwendung C1 für S1 entwickelt und später eine weitere Client-Anwendung C2 für S2. Wenn C2 auch mit S1 funktioniert, dann sind C2 rück- und S1 vorwärtskompatibel. Wenn umgekehrt C1 mit S2 funktioniert, dann sind C1 aufwärts- und S2 abwärtskompatibel. Diese Art der Kompatibilität kann man demzufolge aus zwei Perspektiven diskutieren. Ein USB-3.0-Stick ist beispielsweise abwärtskompatibel, falls er auch in einem USB-2.0-Port funktioniert. Genauso könnte ein USB-3.0-Port abwärtskompatibel sein, falls ein USB-2.0-Stick passt.

Nachrichten mit Datenformaten wie XML, JSON oder Protocol Buffers lassen sich zum Beispiel um neue optionale Felder erweitern. Weil die Felder optional sind, muss ein älterer Client sie nicht nutzen, wenn er eine Nachricht an einen Server schickt. Die API des Servers ist in diesem Fall abwärtskompatibel. Eventuell antwortet der Server dem Client mit einer Nachricht und verwendet dabei Felder, die der Client nicht kennt. Falls Letzterer diese Nachricht trotzdem akzeptiert, ist er vorwärtskompatibel. Der Client ignoriert die unbekannten Felder, sollte sie aber nicht aus dem erhaltenen Dokument entfernen, falls weitere dokumentenzentrierte Interaktionen mit dem Server folgen.