HATEOAS ist nicht nur eine sperrige Abkürzung, es ist auch schwierig herauszufinden, welche Vorteile dieser Ansatz für REST-Schnittstellen bietet. Das ist schade, denn damit lassen sich manche Probleme in der Client-Entwicklung elegant vereinfachen.
Laut Roy Fielding, dem Urheber der REST-Konzepts, sind die meisten sogenannten REST-Schnittstellen nicht RESTful in seinem Sinne. Eine Schnittstelle, die nur Daten liefert, hilft einer Client-Anwendung nicht zu verstehen, wie die Daten auszuwerten sind. Eine REST-Schnittstelle, die dem HATEOAS-Konzept (Hypermedia as the Engine of Application State) folgt, bietet zusätzlich zu den Daten einer Ressource auch Links zu weiteren Endpunkten, über die Client-Anwendungen den Zustand der Ressource verändern, also weitere Operationen ausführen können.
Ein Hauptproblem, HATEOAS bei öffentlichen APIs einzusetzen, ist, dass unabhängig entwickelte Client-Anwendungen nicht gezwungen werden können, den Stil auch zu verwenden. Bei Systemen, deren Server- und Client-Bestandteile aus einer Hand stammen, ist das jedoch kein Argument gegen HATEOAS. Um eine solche Anwendung geht es in diesem Artikel.
In den Diskussionen über HATEOAS finden sich meistens Beispiele, wie REST-Schnittstellen erweitert werden, um Links mitzuliefern. Jeder Link hat einen Namen, sodass eine Client-Anwendung, die diese Namen kennt, passende Links für weitere Operationen dynamisch herausfinden kann. Dadurch muss die Client-Seite nicht den genauen Aufbau der Endpunkte einer REST-Schnittstelle kennen. Die Schnittstelle lässt sich dadurch ändern – beispielsweise können URLs verändert werden – ohne dafür die Client-Anwendungen anpassen zu müssen.
Jedoch ist der konkrete Nutzen dieser Links für die Client-Server-Kommunikation begrenzt. Denn immer noch muss die Client-Anwendung die Einsprungstellen in die Schnittstellen kennen. Und der Client muss wissen, welche Daten er übertragen muss, also die Bezeichnungen und Datentypen der vom Server erwarteten Felder. Und dann braucht der Client noch Informationen über Konsistenzbedingungen wie die erlaubten Wertebereiche der Daten.
Das heißt: Auch wenn die aufrufenden URLs nicht mehr in der Client-Anwendung fest kodiert sind, ist immer noch viel Detailwissen über die Daten in jeder Client-Anwendung fest kodiert. Und weil das so ist, steht oft der ganze Nutzen von HATEOAS in Frage.
Dabei liegt, zumindest nach meiner Erfahrung, der Hauptnutzen von HATEOAS woanders. Wo, das möchte ich schrittweise erläutern, indem ich an einem Beispiel ein typisches Problem aus der Client-Entwicklung schildere.
Ein realistisches Beispiel
Fangen wir mit dem Beispiel an: Angenommen, ein neues Handwerkerportal entsteht. Die Grundidee ist, dass Handwerker ihre Profile hinterlegen und jeder, der einen Handwerker sucht, ein Projekt einstellen und dafür Handwerker kontaktieren kann.
Um nach Handwerkern zu suchen, muss ein Auftraggeber sich anmelden und zunächst ein Projekt einstellen. Der Server liefert dann für ein Projekt passende Vorschläge. Der Client, eine Angular-Anwendung, ruft, um die Vorschläge für ein Projekt mit der ID 12345678 zu erhalten, die folgende URL des Servers auf:
GET /api/projects/12345678/craftpeople
Daraufhin liefert der Server die folgenden Daten zurück:
[ { "id": "1", "name": "Thomas Müller", "rate": "50", "links": [ { "name": "self", "href": "/api/projects/12345678/craftpeople/1" }, { "name": "contact", "href": "/api/projects/12345678/craftpeople/1/contact" } ] }, { "id": "2", "name": "Birgit Schmid", "rate": "60", "links": [ { "name": "self", "href": "/api/projects/12345678/craftpeople/2" }, { "name": "contact", "href": "/api/projects/12345678/craftpeople/2/contact" } ] } ]
Hier kann man erkennen, dass der Server zwei mögliche Handwerker vorschlägt: einen Herr Müller, der 50€ die Stunde nimmt, und eine Frau Schmid für 60€ die Stunde. Zu beiden Datensätzen liefert der Server weiterführende Links. Ein Link mit dem Namen „self“ kann für weitere Details aufgerufen werden, ein Link mit dem Namen „contact“ kann aufgerufen werden, um eine Kontaktaufnahme zu starten.
Die Client-Anwendung verwendet diese Daten, um dem Anwender eine Vorschlagsliste anzuzeigen. Zu jedem Handwerker gibt es einen Link für mehr Details in einem Popup-Fenster und einen Button, um den Kontakt aufzunehmen. Für letzteres kann der Anwender ein kurzes Anschreiben eingeben. Dieser Text wird per POST an den Link mit Namen „contact“ geschickt.
Bis hierhin sieht man, dass HATEOAS auf der Client-Seite hilft, keine URLs für die weiterführenden Operationen zusammenbauen zu müssen. Die erste Version ist schnell fertig gestellt und in Betrieb. Spannend wird es danach, wenn mehr Anforderungen an die Client-Anwendung gestellt werden.
Viele neue Anforderungen
Projekte sollen nach zwei Wochen automatisch deaktiviert werden. Danach dürfen keine neuen Handwerker mehr kontaktiert werden. Dazu wird ein Status-Feld in die Daten über ein Projekt eingefügt. Nur wenn der Status „active“ ist, erscheint in der Folge der Button zum Kontaktieren.
Um mehr potenzielle Auftraggeber anzulocken, sollen Gäste ohne Accounts ein Projekt anlegen können und Handwerker vorgeschlagen bekommen. Die Kontaktaufnahme soll aber erst möglich sein, nachdem sich der Auftraggeber registriert hat. Diese Anforderung wird über ein Berechtigungskonzept gelöst. Nur, wenn der aktuelle Anwender die passende Berechtigung hat, zeigt der Client den Button zum Kontaktieren an. Dafür fragt die Client-Anwendung die Berechtigungen über eine neue API ab und deaktiviert den Button, wenn die passende Berechtigung fehlt.
Die nächste Anforderung: Weil sich zunächst nur wenige Handwerker angemeldet haben, sollen immer alle passenden Handwerker angezeigt werden, auch solche, die nicht verfügbar sind. Die Kontaktaufnahme soll aber nur dann ermöglicht werden, wenn der Handwerker Aufträge annehmen kann. Dafür wird ein neues Status-Feld in die API der vorgeschlagenen Handwerker hinzugefügt und für den Button wird abgefragt, ob dieser Zustand den Wert „AVAILABLE“ hat.
Bald darauf wollen die Betreiber des Portals neue Handwerker erst durch einen persönlichen Anruf überprüfen. Ungeprüfte Handwerker sollen weiterhin angezeigt werden, eine Kontaktaufnahme aber verhindert werden. Also wird ein Boolean-Feld „verified“ in die API aufgenommen und ebenso mit dem Button verknüpft.
Mit der Zeit läuft das Portal immer besser. Aber die Handwerker beschweren sich, dass sie zu viele Anfragen erhalten, die zu beantworten viel Zeit kostet. Darum soll jeder Anwender pro Projekt nur noch eine gewisse Anzahl an Handwerkern kontaktieren können. Also bekommt der Client eine REST-Schnittstelle zur Verfügung, um zu einem Projekt die Anzahl der schon erfolgten Kontaktierungen abzurufen und diesen Wert ebenfalls beim Button zu berücksichtigen. Die Projektleitung warnt die Entwickler schon im Voraus, dass sich die Anzahl der erlaubten Kontaktierungen öfters ändern könnte.
Weitere Handwerker beschweren sich. Sie werden immer wieder von denselben Anwendern für dasselbe Projekt kontaktiert. Daher soll die Client-Anwendung eine erneute Kontaktierung von schon kontaktierten Handwerkern unterbinden. Dafür bekommt die Client-Anwendung eine API mit Zugriff auf die schon kontaktierten Handwerker für ein Projekt. Und ja, der Button darf nur angezeigt werden, wenn der jeweilige Handwerker noch nicht kontaktiert worden ist.
Eine Weile nach der erfolgreichen Markt-Einführung will das Management anfangen, mit dem Portal Geld zu verdienen. Dazu werden unterschiedliche Kontotypen eingeführt: Neben dem Umsonst-Konto gibt es das Amateur-Konto und das Profi-Konto. Diese Konten unterscheiden sich unter anderem darin, wie viele Handwerker ein Anwender pro Monat (also projektübergreifend) kontaktieren darf. Dazu erhält die Client-Anwendung eine weitere API zur Verfügung, um den Kontotyp und die im laufenden Monat schon erfolgten Kontaktierungen abzufragen. Diese Informationen beeinflussen natürlich auch die Sichtbarkeit des Kontaktieren-Buttons.
Schließlich stellt das Management fest, dass die bisher erstellte Angular-Anwendung auf Handies nicht mehr gut genug ist. Daneben sollen eine native Android-App und eine native iPhone-App entstehen, die sich natürlich gleich verhalten sollen.
Jetzt müssen also die folgenden Bedingungen in allen Client-Anwendungen identisch implementiert werden, nur um zu prüfen, ob ein einzelner Button zum Kontaktieren eines Handwerkers angezeigt werden kann:
- Das Projekt muss noch aktiv sein.
- Der Auftraggeber braucht die Berechtigung zum Kontaktieren.
- Der Handwerker muss verfügbar sein.
- Der Handwerker muss überprüft worden sein.
- Die maximale Anzahl an Kontakierungen pro Projekt darf noch nicht erreicht worden sein.
- Der Handwerker darf für dasselbe Projekt noch nicht kontaktiert worden sein.
- Die maximale Anzahl an Kontaktierungen pro Monat laut Kontotyp darf noch nicht erreicht worden sein.
Und natürlich gibt es viele weitere Funktionen in den Client-Anwendungen mit ähnlich komplexen Bedingungen…
Wer mir in der Schilderung dieses Beispiels bis hierhin gefolgt ist, kann vermutlich abschätzen, wie viele Probleme es bereitet, den Button zum Kontaktieren eines Handwerkers auf allen Plattformen konsistent nur dann anzuzeigen, wenn er angezeigt werden darf. Jede kleine Änderung der Anforderungen (wenn sich z.B. die Anzahl der erlaubten Kontaktierungen erhöht) führt zu Anpassungen in jedem Client, verbunden mit neuen Versionen, die auf die Endgeräte ausgeliefert werden müssen. Dass die vielen einzelnen API-Aufrufe der Clients Last auf dem Server erzeugen und die Bedienung verlangsamen, verursacht weitere Probleme.
Ließe sich dieser ganze Aufwand, also die Wartung mehrerer Client-Anwendungen sowie die Implementierung und die Aufrufe der zusätzlichen APIs, vollständig einsparen? Ja, sogar sehr einfach, wenn man HATEOAS konsequent weiter denkt!
HATEOAS konsequent umgesetzt
Der Grundgedanke von HATEOAS ist nicht, dem Client einfach die URLs für weitere Operationen zu liefern, sondern Links für weitere Operationen nur dann anzubieten, wenn der aktuelle Zustand es dem Client erlaubt, die Operationen auch auszuführen!
Das bedeutet hier: Die ganze Entscheidungskompetenz darüber, ob ein Anwender einen Handwerker kontaktieren kann, liegt im Server. Dort werden an einer Stelle alle notwendigen Prüfungen durchgeführt. Und nur genau dann, wenn der Handwerker kontaktiert werden darf, fügt der Server den Link mit dem Namen „contact“ zu einem Handwerker-Datensatz hinzu.
Um zu entscheiden, ob der Button angezeigt wird, prüfen die Client-Anwendung einzig, ob der „contact“-Link zu einem Handwerker vorhanden ist. Diese Abfrage wird in der ersten Version des Clients programmiert und muss nie wieder angepasst werden. Jede der beschriebenen Anforderungsänderungen erfordert nur, den Servercode zu erweitern, der über den Link bestimmt. Es ist eine Stelle für alle Prüfungen, der Code ist einfach zu testen und das Verhalten ist konsistent für alle Clients.
Jegliche Geschäftslogik, die definiert, was mit einem Datensatz als nächstes getan werden kann, bleibt also vollständig und ausschließlich im Server: Ein Server-Deployment und schon sind neue Geschäftsregeln auf allen Client-Anwendungen aktiv.
So einfach kann Client-Entwicklung werden – dank REST mit HATEOAS!