Blog

Die Tücken der Veränderung: Leitfaden zum API-Design

May 20, 2019

Damit einmal getroffene Entscheidungen auf lange Sicht keine Dramen verursachen, sind einige Spielregeln zu beachten. Ganz besonders gilt das für den Entwurf eines API, das naturgemäß über einen langen Zeitraum Verwendung findet.

Die Motivation, für eine eigene Anwendung und vor allem für Programmbibliotheken eine definierte Schnittstelle bereitzustellen, die die Interaktion mit anderen Programmen ermöglicht, ist in vielen Projekten sehr hoch. Doch nicht immer gelingt dieses Vorhaben. Deutlichste Anzeichen für ein verunglücktes API sind häufige Änderungen, die mit Vorgängerversionen inkompatibel sind. Die damit verbundenen Probleme kennt jeder, der einmal innerhalb eines Projekts eine vorhandene Bibliothek gegen eine neuere Version austauschen musste. Je nach Intensität der Nutzung erfordert ein Update kleinere oder sogar sehr große Codeanpassungen.

Aber auch bei korrekter Durchführung ist die bei Verwendung eines API erwartete Flexibilität nicht immer gewährleistet. Theoretisch ist es bei einem korrekten Entwurf möglich, die Implementierung zu einem API problemlos auszutauschen. In realen Projekten tritt dieser Idealfall eher selten ein. Die Ursache ist recht trivial: Solange die Hersteller einer Implementierung dem vom API definierten Standard komplett folgen, ist alles optimal. Ein Beispiel für einen solchen Standard ist die Java Database Connectivity (JDBC). Viele Datenbankhersteller, die für ihr DBMS die JDBC implementieren, haben jedoch mit Schwierigkeiten zu kämpfen. Denn die Systeme unterscheiden sich in einigen Details. So kennt MySQL zum Erzeugen des Primärschlüssels Auto Increment. Eine Funktion, die bei Enterprise-Lösungen fehlt. Solche Feinheiten haben zur Folge, dass viele Contributors den Standard nicht komplett implementieren. Ebenso sind Erweiterungen außerhalb der festgelegten Definition häufig. Deshalb sollte klar sein, dass ein einen Standard definierendes API einen Kompromiss aus minimalen Anforderungen darstellt. So lässt sich sehr leicht einsehen, welche Komplikationen sich bei einem Austausch der Implementierungen ergeben können.

Die Verarbeitung von XML zeigt einen anderen Aspekt auf. Es existieren die Standards DOM, SAX und StAX. Entscheidet man sich bei der Implementierung ursprünglich für DOM und steigt aus Gründen der Performanceverbesserung auf SAX um, stellt man schnell fest: Beide Implementierungen und APIs sind weitgehend inkompatibel. Das Problem lässt sich anhand des Beispiels PDF-Verarbeitung konkretisieren. Obwohl es sich bei PDF um einen Standard handelt, existiert für Programmbibliotheken keine allgemein definierte Schnittstelle. Entsprechend präsentiert sich der Datentyp PdfReader in der iText-Bibliothek [1]. Dieser definiert für das eigene API den Rückgabewert der Methode, um PDF-Dokumente einzulesen. Zwar wirken die erwähnten Beispiele ein wenig pessimistisch, jedoch zeichnen sie ein deutliches Bild von den Grenzen der Flexibilität.

Semantic Versioning

Noch vor dem ersten Entwurf gilt es, sich mit der Versionsnummernvergabe zu beschäftigen. Das bewährte Semantic Versioning [2] legt klar fest, wie die einzelnen Bereiche einer Version inkrementiert werden sollen (Abb. 1).

Abbildung 1

Major: Rückwärts inkompatibel. Das trifft zu, sobald eine Klasse oder Methode geändert oder gelöscht wurde. Diese Veränderungen erfordern Codeanpassungen.

Minor: Erweiterungen. Sämtliche vorhandenen Methoden und deren Signaturen bleiben unverändert. Es werden lediglich weitere Methoden hinzugefügt.

Bugfix: Ausschließlich Fehlerkorrekturen. Es werden keine zusätzlichen Funktionen mit eingebracht.

Die Einhaltung dieser Konvention ermöglicht vorhandenen Konsumenten des API eine bessere Risikoabschätzung. Anhand der Release Notes lässt sich erkennen, ob das eigene Projekt von der Inkompatibilität betroffen ist. Auch vermittelt die Häufigkeit veröffentlichter Major Releases einen Eindruck von der Stabilität des Projekts.

Werde Teil der API-Revolution!
Alle News & Updates zur API Conference 

 

Scheibchenweise

Die Verwendung des Semantic Versionings regelt in groben Umrissen bereits den Release-Prozess. Der nächste Schritt zum Entwurf einer gemeinsamen Programmierschnittstelle führt zur Architektur. Eine moderne Adaption des klassischen MVC-Entwurfsmusters ist die Schichtenarchitektur. Auch dieser Ansatz trennt Verantwortlichkeiten von Datenhaltung, Repräsentation und Geschäftslogik. Er geht allerdings noch einen Schritt weiter: Die definierten Schichten bauen aufeinander auf und können nur von darüberliegenden Schichten angesprochen werden. Dieses Prinzip ist bereits aus dem OSI-Referenzmodell [3] bekannt. Wie ein solches Schichtenmodell für Applikationen aussehen kann, zeigt Abbildung 2.

Abbildung 2

Es sei erwähnt, dass auf GitHub [4] ein Beispielprojekt zu finden ist, das die hier besprochenen Punkte umsetzt. Der Domainlayer auf Ebene 1 hält die Datenobjekte bereit. Auf Ebene 2 enthält die vorgeschlagene Architektur die Implementierungsklassen, während sich auf Ebene 3 die zugehörigen Interfaces befinden. Das führt zum allgemein bekannten Lehrsatz, man solle nicht gegen Interfaces implementieren. Auch wenn das nahezu jeder Programmierer im Schlaf herunterbeten kann, finden sich im beruflichen Alltag nicht wenige Kollegen, denen die Umsetzung des Grundsatzes nicht vollständig bewusst ist. Und das, obwohl sie ihn intuitiv bereits vielfach anwenden. List<Entry> collection = new ArrayList(); ist ein solcher Klassiker, in dem die Interface List durch die ArrayList implementiert ist. Diese Tatsache mag trivial erscheinen. Jedoch existieren genügend Beispiele aus persönlicher Erfahrung, die gezeigt haben, dass eine kurze Erläuterung oft notwendig ist. Bezogen auf Abbildung 2 bedeutet dieses kurze Beispiel, dass das Interface List im Layer 3 und die Klasse ArrayList im Layer 2 aufzufinden sind. Aus dieser Vereinbarung ergibt sich der nächste Punkt, die Namensgebung.

Es ist inzwischen in vielen Projekten üblich, Interfaces mit einem vorangestellten oder angefügten I kenntlich zu machen. Diese Praxis ist jedoch nicht unbedingt empfehlenswert, da sie die Semantik des Source-Codes nicht verbessert, sondern die Lesbarkeit des Texts erschwert. Als Bezeichnung für Implementierungsklassen hat sich der Suffix Impl bewährt. Dennoch geht unser Beispiel aus dem Java API mit gutem Grund einen anderen Weg: Der Name kennzeichnet hier explizit, dass es sich bei der Implementierung der Liste um eine Arrayliste handelt, nicht um einen Vektor oder ähnliches. Zwar ist dieses Vorgehen sehr ratsam, dennoch gibt es Ausnahmen, bei denen man Implementierungsklassen besser mit dem erwähnten Impl-Suffix benennt. Erinnern wir uns an das Beispiel für XML: Wird eine Toolklasse implementiert, die verschiedene Standards vermischt, ist es schwer, eine exakte Bezeichnung zu finden. Deshalb ist es nicht verkehrt, bereits zu Beginn darauf zu achten, dass die Übersicht erhalten bleibt. Hat man sich einmal auf eine Methodik festgelegt, sollte man diese konsequent beibehalten. Das Aufsplitten von Klassen in verschiedene Funktionalitäten birgt die Gefahr, eine große Ansammlung von Klassen zu generieren, die schwer zu handhaben ist. Das Gleiche gilt für Verzeichnisstrukturen: Weniger ist oft mehr.

Servicewüste?

Die vierte Schicht ist für atomare Services reserviert. Als Beispiel hierfür kann die Implementierung eines einfache E-Mail-Clients dienen. Im Application-Layer sind bereits die Funktionalitäten realisiert, die notwendig sind, um Mails zu erzeugen. So zum Beispiel die Funktionalitäten, Empfänger hinzu- oder Attachments anzufügen. Um diesem Gebilde Leben einzuhauchen, ist eine Instanz nötig, die den tatsächlichen Versand der E-Mails durchführt. Auch die Änderung der Konfiguration wird in einen gemeinsamen Mail-Client-Service ausgelagert. Ebenso wie es sich beim Business-Layer um ein API handelt, stellen diese Services eins dar. Der wichtigste Unterschied bei Services ist, dass aufgrund des fehlenden Mehrwerts und des großen Verwaltungsaufwands auf die Bereitstellung von Interfaces verzichtet wird.

Ebenso verhält es sich mit dem Orchestrationslayer, der für Service-Kompositionen vorgehalten wird. Die in den Schichten 4 und 5 bereitgestellten Klassen können im Sinne einer Service-getriebenen Architektur auch RESTful sein. Die Zuordnung des Orchestrationslayers zur View wird besonders bei Webanwendungen ersichtlich. Entscheidet man sich beispielsweise für JSF als Präsentationsschicht, werden Managed Beans benötigt. Diese kombinieren häufig atomare

Services und binden sie an die GUI. Da diese Schicht eher eine statische Durchleitung zu Funktionalitäten darstellt, als den Charakter einer dynamischen Steuereinheit aufzuweisen, ist sie eher der View zuzuordnen.

Um ein stabiles API bereitstellen zu können, ist es notwendig, dieses auf seine Benutzbarkeit hin zu untersuchen. Dabei helfen aufwendige Analysen weniger als einfaches Ausprobieren. In diesem Zusammenhang sei darauf hingewiesen, dass testgetriebene Entwicklung bereits während der Umsetzungsphase eines Projekts wertvolle Informationen liefern kann.

API Design Track auf der API Conference

Testfalle

Das Thema Tests füllt bereits diverse Bücher und wäre einen eigenen Artikel wert. Aber auch bei der Gestaltung eines API spielt die Qualität der Testfälle eine wichtige Rolle. Aus langjähriger Erfahrung entwickeln die meisten Programmierer ihre Funktionalitäten gegen das als Testoberfläche verwendete Applikations-GUI. Die daraus resultierenden Nachteile wurden bereits vielfach besprochen, weshalb wir uns direkt dem Thema Test-driven Design (TDD) zuwenden.

Kern dieses Designparadigmas ist die Entwicklung von Testfällen, die auf der Anforderungsanalyse aufbauen. Eine dagegen geschriebene Implementierung sorgt dafür, dass die Tests nicht mehr fehlschlagen. Ein zugänglicherer Ansatz, TDD in den eigenen Prozess einzubinden, besteht darin, die vorgegebene Spezifikation zu implementieren und daraus die Testfälle abzuleiten. Im Anschluss wird das Resultat mittels Qualitätswerkzeugen wie Cobertura, FindBugs, Checkstyle und PMD verifiziert. Besonders das Erreichen einer hohen Testabdeckung von mehr als 85 Prozent wirkt sich auf die Qualität eines Artefakts aus. Aus diesem Grund muss die Überprüfung der Test-Coverage kontinuierlich erfolgen. Mit Build-Tools wie Maven kann jeder Entwickler das direkt aus seiner IDE heraus bewerkstelligen und entsprechen agieren. Immer wieder werden durch die Verwendung von Bibliotheken mögliche Optimierungen sicht- und umsetzbar. Auch der Vorgang des Testschreibens fördert direkt umsetzbare Einsichten zur Benutzbarkeit der Implementierung.

Probleme mit dem vorgeschlagenen, abgewandelten Vorgehen für TTD ergeben sich in kollaborativen Teams. Diese müssen möglicherweise zunächst Implementierungsergebnisse abwarten, bevor sie sie im eigenen Projekt verwenden können. Ein Effekt, der sich mittels eines modularen Aufbaus und der Vermeidung von transitiven Abhängigkeiten erheblich abschwächen lässt.

Wachmannschaft

Ist die Implementierung eines Interface durch eine ausreichende Anzahl von Testfällen abgedeckt, stellt dies einen aussagekräftigen Funktionsbeweis des API-Designs dar. Dabei sollten die Ergebnisse der Beweisführung bestmöglich dokumentiert werden. In Form der freien Bibliothek apiguardian existiert eine Möglichkeit, den Source-Code mit der Annotation @API anzureichern. Diese zeigt den Status eines Attributs, einer Methode oder einer Klasse an. Die unkomplizierte Einbindung ins Projekt lässt sich via Maven mit den folgenden Zeilen durchführen:

<dependency>
<groupId>org.apiguardian</groupId>
<artifactId>apiguardian-api</artifactId>
<version>1.0.0</version>
</dependency>

Nach Einbindung der Bibliothek können sämtliche API-Komponenten annotiert werden. Die Annotation @API kennt die beiden Parameter status und since. Since kennzeichnet, ab wann ein Interface oder eine Methode verfügbar ist. Das zeigt, wie hilfreich die Verwendung des Semantic Versioning im praktischen Einsatz ist. Der Parameter status wiederum kennt fünf Zustände:

DEPRECATED: Veraltet, sollte nicht weiterverwendet werden.

EXPERIMENTAL: Kennzeichnet neue Funktionen, auf die der Hersteller gerne Feedback erhalten würde. Mit Vorsicht verwenden, da hier stets Änderungen erfolgen können.

INTERNAL: Nur zur internen Verwendung, kann ohne Vorwarnung entfallen.

STABLE: Rückwärtskompatibles Feature, das für die bestehende Major-Version unverändert bleibt.

MAINTAINED: Sichert die Rückwärtsstabilität auch für das künftige Major-Release zu.

Ein Screenshot (Abb. 3) aus der NetBeans IDE, die die API-Annotation für die Interfacemethode log() eines Loggers einblendet, zeigt die Vorteile, die diese Art von API-Dokumentation bietet.

 

Abbildung 3

In Bezug auf die vorgestellte Schichtenarchitektur bedeutet das: Sämtliche Interfaces aus dem Businesslayer und alle Service-Klassen aus dem Service-Layer stellen das API dar und sollten entsprechend annotiert werden.

Zusammenfassung

Das Thema API-Design hält einige nicht zu unterschätzende Tücken bereit. Die Brisanz der Materie hat eine Vielzahl von Checklisten und Anleitungen für API-Entwürfe hervorgebracht. Dieser Beitrag versucht eine Brücke zwischen den Vorgehensmodellen und den Zusammenhängen im Softwareentwurf zu bauen. Denn eins ist gewiss: Nichts ist so beständig wie die Veränderung. Und nicht alle kontinuierlich wiederholten Annahmen in Projektmeetings treffen dadurch ein, dass man sie unermüdlich aufsagt. Qualität und Beständigkeit lassen sich nicht ausschließlich durch den Einsatz von Technologien erreichen, sondern durch die korrekte Verwendung der ausgewählten Werkzeuge und Methoden.

 

Links & Literatur

[1] iText-PDF-Bibliothek https://itextpdf.com

[2] Semantic Versioning: https://semver.org

[3] OSI-Schichtmodell : https://www.elektronik-kompendium.de/sites/kom/0301201.htm

[4] TP-CORE-Beispielanwendung: https://github.com/ElmarDott/TP-CORE/

Alle News & Updates zur API Conference:

Behind the Tracks

API Management

Ein detaillierter Blick auf die Entwicklung von APIs

API Development

Architektur von APIs und API-Systemen

API Design

Von Policys und Identitys bis Monitoring

API Platforms & Business

API Plattformen in Verbindung mit SaaS