Suche
Fieldings Vermächtnis

Wer REST will, muss mit HATEOAS ernst machen

Stefan Ullrich

© Shutterstock/Tashatuvango

HATEOAS ist laut Roy Thomas Fielding das zentrale REST Constraint – und das am häufigsten missachtete in Architekturen, die sich selbst „RESTful“ nennen. Hier wird eine Implementierung vorgestellt, die ernst macht mit HATEOAS und dazu einen weiteren HTTP-Standard nutzt: den Linkheader.

Bereits jetzt, zu Lebzeiten Roy T. Fieldings, ist sicher, dass seine Arbeit zu REST kommende Generationen beeinflussen wird. Mit dem Titel des Artikels ist also ein „Vermächtnis in spe“ gemeint. Fielding formulierte es im Jahr 2000 erstmals in seiner Doktorarbeit und hat es seitdem oft wiederholt: „REST APIs must be hypertext-driven“ scheint er uns anschreien zu wollen in dem berühmten Blog-Post aus dem Jahr 2008, in dem es weiter heißt: „I am getting frustrated by the number of people calling any HTTP-based interface a REST API. Today’s example is the SocialSite REST API. That is RPC. It screams RPC. There is so much coupling on display that it should be given an X rating. What needs to be done to make the REST architectural style clear on the notion that hypertext is a constraint? In other words, if the engine of application state (and hence the API) is not being driven by hypertext, then it cannot be RESTful and cannot be a REST API. Period. Is there some broken manual somewhere that needs to be fixed?“.


Aber wo steht da was von HATEOAS? Hier: „… if the engine of application state is not being driven by hypertext, then it cannot be RESTful“. Hypermedia As The Engine Of Application State – das steckt hinter dem hässlichen Akronym, das mit „Hate“, zu Deutsch: „Hass“, beginnt.

Was bedeutet HATEOAS?

Hypermedia As The Engine Of Application State ist also eine essenzielle Bedingung für jede REST-Architektur. Dann sollte es von zentraler Bedeutung sein, diesen Satz genau zu verstehen:

1. Hypermedia: Der Begriff Hypermedia setzt sich zusammen aus den Begriffen „Hypertext“ und „Multimedia“. Die griechische Präposition „hyper“ hat die Bedeutung „über … hinaus“ – Hypertext ist also ein Text, der über sich selbst hinaus weist, und zwar durch Verlinkung auf einen anderen Text. Hypermedia erweitert dieses Konzept durch die Verallgemeinerung von Text durch Multimedia.
2. Engine: Die deutsche Übersetzung von Engine ist (unter anderem) Maschine bzw. Automat – und genau das ist auch gemeint: ein Zustandsautomat.
3. Application heißt Anwendung – sowohl im Sinne eines Softwareprogramms als auch der Anwendung der Prinzipien einer (hier: REST-)Architektur. Im Kontext eines RESTful-API kann man Application gleichsetzen mit Ressource.
4. State: State heißt Zustand und verbindet Engine und Application: Ein Automat besteht aus Zuständen und Übergängen und beschreibt damit das Verhalten der Anwendung.

Ein Zustandsautomat wird in UML als Zustandsdiagramm modelliert: Kanten verbinden die möglichen Zustände und stellen damit Übergänge von einem Zustand zum nächsten dar. Von einem Zustand können mehrere Kanten zu verschiedenen Folgezuständen verlaufen (Abb. 1). Der Zustand einer RESTful Application ist vollständig in ihrer jeweiligen Repräsentation enthalten – die Repräsentation ist der Zustand der Ressource (daher der Begriff REpresentational State Transfer)! Daraus folgt: das Zustandsdiagramm entspricht der Anwendung, die den Kanon möglicher Zustände (Repräsentationen) und deren Übergänge enthält. Alle Übergänge zusammen bilden die Engine, die Zustandsveränderungen hervorruft. Und wie führt die Engine Zustandsübergänge durch? Hypermedia. Links! Fazit: Ohne Links keine Zustandsübergänge – ohne Zustandsübergänge keine Anwendung.

Abb. 1: Ein Zustandsdiagramm in UML

Was ist das Besondere an REST?

Fielding hat in seiner Dissertation unter anderem beschrieben, wie das Web als Anwendung des REST-Architekturstils funktioniert. Es wird definiert als ein System verteilter Ressourcen, die über das Hypertext (!) Transfer Protocol (HTTP) kommunizieren und durch Links vernetzt bzw. vernetzbar sind. Hier wird deutlich, dass das Web inhärent offen und skalierbar ist und HTTP sein API. Eine RESTful Application muss diese Eigenschaften auf ihre Architektur übertragen.
Im Zentrum steht die Ressource, die über Links erreichbar (evtl. manipulierbar) und mit anderen Ressourcen verbunden ist. Eine Ressource erreichen heißt eine Repräsentation zu erhalten. Eine Ressource manipulieren heißt eine Repräsentation zu manipulieren und diese an die Ressource zurückzuschicken – in der Hoffnung, dass die Ressource daraufhin die Gestalt der Repräsentation übernimmt. Hier wird der fundamentale Unterschied zu Remote Procedure Calls (RPC, RFC, RMI) deutlich: Letztere stellen eigentlich ein Host-Terminal-Konzept dar: Die Prozedur läuft auf dem Hostsystem, das Terminal bietet die Möglichkeit, die Prozedur aus der Ferne aufzurufen. Terminal und Host sind aufs Engste gekoppelt – der Host entblößt seine internen Funktionen gegenüber dem Terminal und gibt damit die Kontrolle vollständig ab. SOAP Web Services replizieren im Prinzip genau dieses Modell.
Die Ressource dagegen hat jederzeit die volle Kontrolle, hat sie doch „nur“ eine Repräsentation von sich heraus gegeben und nicht ihr internes API. Clients können eine Repräsentation ändern oder löschen, soviel sie wollen – solange die Ressource es nicht zulässt, hat das keinerlei Auswirkungen auf die Ressource selbst. Da die Ressource nur über Links erreichbar ist, kann sie darüber das API der Kommunikation vollständig selbst bestimmen. Sie kann z. B. Links ändern oder an bestimmte Clients nicht ausliefern. REST ermöglicht der Ressource also maximale Autonomie und Kontrolle bei gleichzeitig minimaler Kopplung der Clients. Dazu müssen Clients Links als atomar – nicht veränderbar – betrachten.

URLs sind die Primary Keys des Webs

URLs definieren Ressourcen eindeutig – sie haben die gleiche Bedeutung wie der Primary Key einer Entität. Eine RESTful Application muss sich verhalten wie eine Internetseite: Sie hat eine Homepage, deren Adresse über einen Namen gesucht werden kann. Ab da gibt es nur noch Links, denen man folgen kann (und muss). Ob und welche Links existieren, entscheidet die Ressource (zur Laufzeit), nicht ein A-priori-API. Da nur die Ressource die Links erzeugt, kann sie den Workflow der Interaktion individuell und dynamisch bestimmen. Die Ressource ist also nicht abhängig von einem Schnittstellen-API, sondern definiert und beherrscht dieses. Selbst Änderungen von URIs können so (unbemerkt vom Client) vollzogen werden, da dieser nur den Links folgt, die er von der Ressource bekommen hat.
Viele Anwendungen, die sich als RESTful bezeichnen, ignorieren HATEOAS komplett. Es werden keinerlei Links generiert, die den Client durch den Workflow führen. Das hat zwei Konsequenzen: Da Links nicht von der Ressource kommen, sind Clients gezwungen, sich URLs „zusammenzubasteln“ (was nur in Kenntnis von Interna der Ressource geht). Außerdem gibt es ohne Links gar keinen Workflow, sondern allenfalls ein statisches API. Neben dem Fehlen des Workflows wird damit eine enge und brüchige Kopplung erzeugt: Ändern sich Resource-URIs, funktionieren die Clients nicht mehr; sollen sich die Clients nicht ändern müssen, darf sich die Ressource nicht ändern.

Linkheader statt Atom

Ein oft empfohlener Weg, Hypermedia zu implementieren, ist die Verwendung von Atom: Das XML-Schema von Atom enthält im Header eines Feeds ein Linkelement, in dem neben einem URL eine Relation (und ein Mediatype) angegeben werden kann (Listing 1).

<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  ...
  <link rel="self" href="http://server/feeds/23452345"/>
  <entry>
    ...
  </entry>
</feed>

Allerdings: Abgesehen davon, dass ich mein Repräsentationsformat nicht auf XML beschränkt wissen will, frage ich mich, warum ich meine Geschäftsobjekte in einer Feed-Struktur verschicken sollte. Die Atom-Spezifikation besagt: „The primary use case that Atom addresses is the syndication of Web content such as weblogs and news headlines to Web sites“. Warum diesen Use Case so dehnen, um nicht zu sagen: zweckentfremden? Nur weil Atom ein Linkelement definiert? Dazu kommt, dass Atom ein ähnliches „Envelope“-Konstrukt einführt wie SOAP – der HTTP-Body enthält einen Container (<feed>), der Metadaten und Payload (das „eigentliche“ Feed Entry), also quasi wieder Header und Body enthält. Damit wird HTTP als Application Protocol missachtet.
Dabei hat HTTP alles, was man braucht: eine HTTP-Response besteht aus HTTP-Header und HTTP-Body, und im Header gibt es Links. Die HTTP-Spezifikation definiert den Linkheader als „a means for describing a relationship between two resources. Links at the metainformation level typically indicate relationships like hierarchical structure and navigation paths“. Die Übergänge von einem Zustand der Ressource zum nächsten sind genau das: Navigationspfade. Sie sind nicht Teil der Repräsentation, sondern HATEOAS-Metainformationen und damit im HTTP-Header am richtigen Platz. Wir unterscheiden Navigation-Links und Content-Links – erstere sind Workflowlinks im Sinne von HATEOS, letztere sind Links auf andere Ressourcen bzw. Subressourcen im Sinne von Lazy Loading (mehr dazu in einem folgenden Artikel).

Der Linkheader enthält eine Liste von Linkeinträgen. Ein Eintrag besteht aus der URL des Links, gefolgt von Parametern und Extensions (Listing 2). Zu den Parametern gehört die Linkrelation, die ein Schlüssel für die Bedeutung des Links ist. Eine standardisierte Liste von Relationen wird von der IANA in der Link Relation Registry verwaltet. Wir verwenden u. a. folgende Relationen:

self – der mit dieser Relation versehene Link zeigt auf die aktuelle Repräsentation der Ressource
new – der Link liefert die Datenstruktur zur Anlage einer neuen Ressource („Formular“)
next – der Link stellt den nächsten Schritt im Workflow dar
previous – der Link stellt den vorherigen Schritt im Workflow dar
all – ein Link auf eine Listenrepräsentation der Ressource zum Auswählen einzelner Elemente

Link-Extensions sind frei definierbare Properties. Diese verwenden wir, um auch die erlaubten HTTP-Verben eines Links zu spezifizieren (Listing 2).

Link           = "Link" ":" #("<" URI ">" *( ";" link-param )        link-param     = ( ( "rel" "=" relationship ) 
                 | ( "rev" "=" relationship )
                 | ( "title" "=" quoted-string )
                 | ( "anchor" "=" <"> URI <"> )
                 | ( link-extension ) )
link-extension = token [ "=" ( token | quoted-string ) ]        relationship   = sgml-name
               | ( <"> sgml-name *( SP sgml-name) <"> )        sgml-name      = ALPHA *( ALPHA | DIGIT | "." | "-" )

Link: <http://server/service/mandat> 
      rel="self"; 
      type="text/html;charset=UTF-8"; 
      title="Servicebeschreibung und Einstiegslinks"; 
      verb="GET, HEAD", 
      <http://server/service/mandat/mandate> 
      rel="all"; 
      type="application/json;charset=UTF-8"; 
      title="Alle Mandate"; 
      verb="GET", 
      <http://server/service/mandat/mandat> 
      rel="new"; 
      type="application/json;charset=UTF-8"; 
      title="Neues Mandat"; 
      verb="GET"

Aufmacherbild: HTTP Blue Button. More Buttons Like that is in My Folio. von Shutterstock / Urheberrecht: Tashatuvango

[ header = Seite 2: Surfen mit einer REST-Ressource ]

Surfen mit einer REST-Ressource

Nehmen wir als Beispiel einen Druckservice, der vom Einzeldruck über Formulardruck mit Datenanreicherung bis hin zum Massendruck mit Kuvertierung und Versand alle Services rund ums Drucken in einem Unternehmen bereitstellt. Dieser Druckservice bietet eine REST-Schnittstelle, an die man einen Druckauftrag POSTen kann, der dann vom Service verarbeitet wird. Würde der Druckworkflow mit diesem POST beginnen (siehe [6], S. 114), hätten wir schon so viele „Out-of-Band“-Informationen besitzen müssen, dass von HATEOAS (und loser Kopplung) keine Rede sein kann:

• Wie beginne ich die Kommunikation?
• Wo finde ich den Druckservice?
• Welche Optionen habe ich?
• Wie sieht ein Druckauftrag aus?
• Wohin muss ich den Druckauftrag senden?

Dies sind Fragen, über die wir keine Annahmen machen dürfen, denn deren Antworten liegen in der Hoheit der Ressource. Die Kommunikation mit der Ressource muss also mit der Suche nach ihr beginnen. Im Internet können wir eine Adresse googeln – in unserer serviceorientierten Architektur gibt es auch eine „Suchmaschine“: die Service-Registry. Eine Anfrage mit dem Namen des gesuchten Service führt zu einer Antwort in Form eines URL – schon hier beginnt Hypermedia: Der Inhalt der URL ist irrelevant, wichtig ist nur zu wissen, dass dies eine Adresse des gesuchten Service ist. Nun können wir dem erhaltenen Link folgen und erreichen die „Homepage“ des Druckservice. Der Body der HTTP-Response könnte tatsächlich eine HTML-Seite mit einer Beschreibung des Service und seiner Funktionalität enthalten, aber wir interessieren uns nicht für den Body, sondern für den Linkheader. Deshalb würde statt des GET auf den Link-URI ein HEAD ausreichen, denn dieses HTTP-Verb liefert nur den Header zurück. Der Linkheader enthält die Einstiegslinks für die Kommunikation mit der Ressource (Listing 3).

> GET /service/registry/de.haspa.hsf.druck HTTP/1.1
>
< HTTP/1.1 200 OK
http://server/service/druck

> GET /service/druck HTTP/1.1
>
< HTTP/1.1 200 OK
< Location: http://server/service/druck
< Link: <http://server/service/druck> 
         rel="self"; type="text/html;charset=UTF-8";   
         title="Servicebeschreibung"; verb="GET",   
        <http://server/service/druck/druckauftraege> 
         rel="all"; type="application/json;charset=UTF-8";  
         title="Alle erledigten Druckauftraege"; verb="GET", 
        <http://server/service/druck/druckauftrag> 
         rel="new"; type="application/json;charset=UTF-8"; 
         title="Neuer Druckauftrag"; verb="GET"

Wir folgen der new-Relation, die uns das JSON eines Druckauftrags liefert. Dies ist unser „Formular“ und damit das Schnittstellenobjekt, das von der Ressource beim POST erwartet wird. Es wird also nicht a priori (durch WADL oder Mediatype) definiert, sondern dynamisch abgefragt (übrigens: Wohin wir den Druckauftrag denn POSTen sollen, wissen wir auch noch nicht). Jeder Druckauftrag muss ein Drucktemplate referenzieren, da das Drucksystem diese Templates mit den Nettodaten aus dem Druckauftrag anreichert. Deshalb enthält der Linkheader der Response einen Link auf die Auswahl eines Templates unter der Relation „next„. Nach Auswahl des gewünschten Templates haben wir nicht nur alle notwendigen Datenstrukturen beisammen, um den Druckauftrag zu senden, sondern auch den URL zum POSTen des Druckauftrags erhalten – wiederum unter der Relation „next“ (Listing 4).

> GET /service/druck/druckauftrag HTTP/1.1
>
< HTTP/1.1 200 OK
< location: http://server/service/druck/druckauftrag
< Link: <http://server/service/druck/drucktemplates> 
         rel="next"; type="application/json;charset=UTF-8"; 
         title="Verfügbare drucktemplates"; verb="GET"
{
  "auftraggeber":{
    "personalnummer":"","nachname":"","vorname":"","email":"",
    "standarddrucker":""
  },
  "dokumentDatum":"","prioritaet":"","status":"","anzahl":"",
  "druckername":"","dokumente":{"list":[]}
}

> GET /service/druck/drucktemplates HTTP/1.1
>
< HTTP/1.1 200 OK
< Location: http://server/service/druck/drucktemplates
< Link: <http://server/service/druck/drucktemplates> 
         rel="self"; type="application/json;charset=UTF-8"; 
         title="die verfügbaren drucktemplates"; verb="GET"
{
  "location":"http://server/service/druck/drucktemplates",
  "list":[
    {
      "location":"http://server/service/druck/
                            drucktemplates/UMSATZDETAIL",
      "drucktemplate":"UMSATZDETAIL"
    },
...
  ]
}

> GET /service/druck/drucktemplates/UMSATZDETAIL HTTP/1.1
>
< HTTP/1.1 200 OK
< Location: http://server/service/druck/drucktemplates/UMSATZDETAIL
< Link: <http://server/service/druck/drucktemplates/UMSATZDETAIL> 
      rel="self"; type="application/json;charset=UTF-8"; 
      title="die verfügbaren drucktemplates"; verb="GET", 
     <http://server/service/druck/> 
      rel="next"; type="application/json;charset=UTF-8"; 
      title="Sende druckauftrag"; verb="POST"
{
  ... (Drucktemplate-JSON)
}

Nachdem wir den Druckauftrag befüllt haben, benutzen wir den „next„-Link, um den Druckauftrag zur Ressource zu POSTen. Die Response liefert uns den HTTP-Status 202 (Accepted), wenn der Druckauftrag angenommen wurde. Angenommen ist ein Druckauftrag, wenn er syntaktisch korrekt ist und der Verarbeitung übergeben werden konnte. Die Verarbeitung hat dann aber erst begonnen, sodass wir noch nicht sagen können, ob der Auftrag auch erfolgreich abgeschlossen werden wird (das ist der Grund, warum nicht der HTTP-Status 201 Created zurückgesandt wird). Diese Asynchronität ist natürlich beabsichtigt und verhindert, dass der Serviceconsumer auf den Abschluss der Verarbeitung warten muss. Stattdessen enthält der Linkheader der Response einen Link auf den laufenden Druckauftrag, mit dem der Status abgefragt werden kann (wenn gewünscht). Solange die Verarbeitung noch nicht abgeschlossen ist, liefert dieser Request den HTTP-Status 304 (Not Modified) zurück, zusammen mit demselben Link im Linkheader. Wenn die Ressource feststellt, dass der Auftrag ausgeführt wurde, sendet sie stattdessen einen Redirect (303 See Other) auf den neuen URL des nun erledigten Druckauftrags (Listing 5).

> POST /service/druck HTTP/1.1
> Content-Type:application/json;charset=UTF-8
> Content-Length: 6836
{
  ... (Druckauftrag-JSON)
}
< HTTP/1.1 202 Accepted
< Location: http://server/service/druck/druckauftrag/2304587356
< Link: <http://server/service/druck/druckauftrag/2304587356> 
      rel="next"; type="application/json;charset=UTF-8"; 
      title="Druckauftrag 2304587356"; verb="GET"

> GET /service/druck/druckauftrag/2304587356 HTTP/1.1
>
< HTTP/1.1 304 Not Modified
< Link: <http://server/service/druck/druckauftrag/2304587356> 
      rel="next"; type="application/json;charset=UTF-8"; 
      title="Druckauftrag 2304587356"; verb="GET"

> GET /service/druck/druckauftrag/2304587356 HTTP/1.1
>
< HTTP/1.1 303 See Other
< Location: http://server/service/druck/druckauftraege/2304587356

> GET /service/druck/druckauftraege/2304587356 HTTP/1.1
>
< HTTP/1.1 201 Created
< Location: http://server/service/druck/druckauftraege/2304587356 

Wir sehen hier also, wie der gesamte Vorgang – inklusive asynchroner Verarbeitung – von der Ressource mittels Links gesteuert wird. Der Client hat nicht viel mehr zu tun als dem jeweiligen „next„-Link zu folgen – und das völlig transparent: Welche Gestalt dieser Link hat, ist irrelevant. Falls das System demnächst skalieren muss, kann es sein, dass verarbeitete Druckaufträge von einem anderen Server ausgeliefert werden. Dann würde in der Location der Redirect-Response vielleicht folgender Link stehen:

Location: http://server2/service/druck/druckauftraege/2304587356

Ein anderer Server, eine andere Serviceinstanz, aber keinerlei Unterschied in der Kommunikation – dank HATEOAS!
Wie jede Hypermediaanwendung muss sich der Workflow des Druckservice in einem Zustandsdiagramm modellieren lassen. Wir erinnern uns: Zustände werden durch Links erreicht – gibt es keinen Link, der zu einem bestimmten Zustand führt, kann dieser auch nicht erreicht werden!

Abb. 2: Zustandsdiagramm des Druckservice

Abbildung 2 zeigt das Zustandsdiagramm des Druckservice. Der Workflow beginnt mit der erfolgreichen Service-Discovery. Dann ergibt die Abfrage der Einstiegslinks drei Möglichkeiten: den Aufruf der Servicebeschreibung, die Erzeugung eines neuen Druckauftrags und die Abfrage erledigter Druckaufträge (hier wäre natürlich als weitere Option z. B. eine Suche nach erledigten Druckaufträgen anhand verschiedener Kriterien denkbar). Diese drei Optionen werden im Diagramm als Kanten (Übergänge) modelliert und sind bekanntermaßen Links. Den Weg zu einem neuen Druckauftrag haben wir im HATEOAS-Workflow (Listings 4 und 5) schon gesehen. Er führt bis zum Zustand „Erledigter Druckauftrag“ und erreicht daraufhin (vorausgesetzt der Responsestatus war 201) den Endzustand des Diagramms (Fehlerfälle sind hier nicht modelliert).
Im Diagramm führen allerdings noch drei Kanten vom Status „Erledigter Druckauftrag“ weg: eine zu sich selbst und je eine zum Status „Alle Druckaufträge“ bzw. „Gelöschter Druckauftrag“ – wo sind die dazugehörigen Links? Die Guard Condition dieser drei Übergänge ist ein HTTP-Status 200 in der vorherigen Response – wenn wir von der Erzeugung eines neuen Druckauftrags in diesem Zustand ankommen, haben wir aber den Status 201. Diese Modellierung korrespondiert auch mit dem HATEOAS-Protokoll (Listing 5): die Response nach dem Redirect enthält nur die Location des erledigten Druckauftrags, keine weiterführenden Links.
Anders sieht es aus, wenn wir als Einstiegslink die all-Relation wählen: Nach der Rückgabe einer Liste von Links auf erledigte Druckaufträge führt uns der Aufruf eines dieser Links zum selben Zustand „Erledigter Druckauftrag“, nun aber mit einem Responsestatus 200 und deshalb auch mit Linkheadern zur Rückkehr zur Liste oder zum Löschen des aktuellen Druckauftrags (Listing 6).

> GET /service/druck/druckauftraege/2304587356 HTTP/1.1
>
< HTTP/1.1 200 OK
< Location: http://server/service/druck/druckauftraege/2304587356 
< Link: <http://server/service/druck/druckauftraege/2304587356> 
         rel="self"; type="text/html;charset=UTF-8";   
         title="Druckauftrag 2304587356"; verb="GET",   
        <http://server/service/druck/druckauftraege/2304587356> 
         rel="next"; type="application/json;charset=UTF-8"; 
         title="Druckauftrag löschen"; verb="DELETE",
        <http://server/service/druck/druckauftraege> 
         rel="all"; type="application/json;charset=UTF-8";  
         title="Alle erledigten Druckauftraege"; verb="GET" 

HATEOAS-Design

Selbstverständlich zeigt das Diagramm in Abbildung 2 nur einen Ausschnitt der Funktionalität des Druckservice. Es wird jedoch deutlich, wie HATEOAS modelliert und umgesetzt werden kann. Beim Design einer REST-Ressource ist ein UML-Zustandsdiagramm ein probates Mittel, um die erwünschten Zustände zu identifizieren und dabei gleich die Hypermedialinks zu erkennen, die nötig sind, um diese Zustände zu erreichen. Allein in der Wahl des Diagrammtyps wird deutlich, wie sehr sich eine konsequente Umsetzung der REST-Prinzipien von imperativer Programmierung (OOP, RPC, SOAP) unterscheidet: REST steht für Zustand, imperative Programmierung für das Tun, das Verb, das in UML als Aktivität im Aktivitätsdiagramm dargestellt wird. Aktivitäts- und Zustandsdiagramm verhalten sich orthogonal zueinander: im Aktivitätsdiagramm ist der Übergang zwischen zwei Aktivitäten ein Zustand, im Zustandsdiagramm ist der Übergang zwischen zwei Zuständen eine Aktivität (nämlich das Folgen eines Links). Aktivitätsdiagramme stellen also das Tun in den Vordergrund – ein API auf dieser Basis ist imperativ und publiziert das Tun (Funktionsaufrufe). Zustandsdiagramme stellen den Zustand in den Mittelpunkt (die Repräsentation). Ein darauf basierendes REST-API stellt für die Übergangslinks zur Verfügung, verrät aber nichts über die Interna der Ressource.
REST hat seinen Wert als Architekturstil für verteilte Anwendungen bewiesen und sich auch im Enterprise-Umfeld weit verbreitet. Allerdings wird es immer noch vielfach lediglich als Variante des Transportprotokolls für Web Services (miss-)verstanden. Dabei ist REST nicht eine Variante, sondern ein Paradigma, das sich (wie der Unterschied zwischen Aktivitäts- und Zustandsdiagrammen) orthogonal zu imperativer Programmierung positioniert. Zu Recht beharrt Roy Fieldings auf HATEOAS als zentraler Bedingung für jedes RESTful-API und verlangt von uns, ernst zu machen mit REST.

Geschrieben von
Stefan Ullrich
Stefan Ullrich
Stefan Ullrich hat Informatik studiert und ist Sun Certified Enterprise Architect mit 17 Jahren Java-Erfahrung. Er hat eine RESTful SOA („ROA“) implementiert (siehe „Weckruf der Moderne“ im Java Magazin 5.2014) und diverse weitere Artikel im Java Magazin veröffentlicht.
Kommentare
  1. Ulf Pietruschka2016-11-16 16:10:41

    Wer oder was ist denn der Client hier?
    Welche Client-Software (einem Menschen als Client wird man das ja wohl kaum zumuten) soll denn damit klarkommen, dass sie erstmal quasi nichts weiss über die angebotenen Services?
    Und wenn gilt: "... Angenommen ist ein Druckauftrag, wenn er syntaktisch korrekt ist ...", was ist denn dann syntaktisch korrekt? Der Client weiss ja sicher auch nichts über die korrekte Syntax, oder? Oder doch?

    Dafür gibt es ja eben eine WADL, ähnlich wie die WSDL bei SOAP, also einen Contract, aber der kommt bei HATEOAS aus dem nichts?

    Das beschriebene Beispiel für einen Druckservice ist doch nichts als eine sinnfreie akademische Übung.

  2. Stefan Ullrich2016-11-18 20:54:00

    Tja, der beschriebene Druckservice ist nicht "nichts als eine sinnfreie akademische Übung", sondern seit 2013 in einer großen Bank in Produktion... :-)

  3. PS2016-12-03 20:13:10

    Die Links in den HTTP-Header zu verlagern, halte ich für eine schöne Lösung, da ich der Meinung bin, dass Meta-Informationen, die nichts mit der Ressource an sich zu tun haben, in den HTTP-Header gehören.

    Noch schöner wäre es, wenn man die Meta-Informationen explizit anfordern müsste, da ich diese ja nicht immer benötige (Z.B. wenn ich die gesuchte Ressource gefunden habe). In der Facebook Graph-API kann dies z.B. mit dem Query-Parameter metadata=1 gemacht werden (https://developers.facebook.com/docs/graph-api/using-graph-api#introspection). Darüber, ob ein Query-Parameter für Informationen, die im HTTP-Header stehen werden, das Richtige ist, lässt sich streiten, aber es geht mir hier auch nur darum, dass man solche Meta-Daten vielleicht nur bei expliziter Anforderung zurückgeben sollte.

    Den großen Vorteil von REST mit HATEOAS gegenüber Web-RPC Lösungen sehe ich allerdings leider nicht. Bei jeder API-Änderung, die über das Verändern von URIs hinaus geht, muss man trotzdem die Clients anpassen. Clients können doch nicht wissen, wie sie zusätzliche Informationen darstellen sollen (Außer es handelt sich um sehr generische Clients. Ich bezweifle aber, dass es so generische Clients gibt. Ein Webbrowser ohne HTML5-Unterstützung kann nichts mit video- oder audio-Elementen anfangen. Noch problematischer wird es, wenn Anfragen an den Server neue Informationen benötigen.

    Verändern sich API-Pfade so häufig, dass es sich lohnt HATEOAS umzusetzen? Ein fester Einstiegspunkt bleibt immer. Ob es nun der Basis-Pfad meiner API oder irgendeine Service-Registry ist.

  4. Ulf Pietruschka2016-12-05 15:25:29

    Natürlich kann man das so implementieren.

    Habe ich in ähnlicherweise für die Client-Seite auch schon gemacht (machen müssen).

    Die dynamischen URLs erhöhen den Implementierungsaufwand und eröffnen weitere mögliche Fehlerquellen. Und dynamisch sind sie nicht wirklich, das hat ja auch der Kommentator zuvor ("PS") richtig beschrieben: Ich benötige eine klare (proprietäre) Identifikation für den gewünschten Service, ich will ja nicht "irgendwas" bekommen, sondern zuverlässig den Service, den ich erwarte, und den ich genau kenne, sonst kann ich ihn nicht zuverlässig verwenden.

    URLs in einem Applikationsserver anders zu mappen ist auch ohne HATEOAS kein Problem.

    Ich sehe hier daher nur Nachteile, daher "sinnfrei" :-)

    Aber vielleicht habe ich ja etwas übersehen ...

  5. Roland "Glatzemann" Rosenkranz2016-12-12 16:46:36

    > Verändern sich API-Pfade so häufig, dass es sich lohnt HATEOAS umzusetzen? Ein fester Einstiegspunkt bleibt immer. Ob es nun der Basis-Pfad meiner API oder irgendeine Service-Registry ist. Den großen Vorteil von REST mit HATEOAS gegenüber Web-RPC Lösungen sehe ich allerdings leider nicht. Bei jeder API-Änderung, die über das Verändern von URIs hinaus geht, muss man trotzdem die Clients anpassen. <

    Eben nicht. Ich rufe ein "Druckobjekt" über die Schnittstelle ab. Dort sind fünf Felder definiert. Diese kann ich im Client (mit einer hübschen, spezialisierten und Handcodierten Oberfläche editieren oder erfassen). Dem Client wurde nun beigebracht, daß er den Link zur Anlage von neuen Druckobjekten über eine bestimmte rel-Kennung erhält. Die fünf (festen) Felder werden an diesen Link übertragen und das Druckobjekt wird angelegt. Status abrufen kann man dann, wie im Beitrag beschrieben.

    Entscheidet sich nun jemand, daß man bei einem Druckobjekt ein neues Feld gebrauchen könnte um beispielsweise zusätzlich "Stickereien" anbieten zu können, so kann dies einfach in besagtes "Druckobjekt" hinzugenommen werden. Der Client kennt dieses Feld nicht, wird es nicht befüllen und es gibt keinerlei Probleme. Ich kann weiterhin über meinen Druckobjekt-Client Druckaufträge einstellen.

    Mit SOAP, RPC, EJB oder ähnlichen hätte ich jetzt schon ein mittelschweres Problem. Ich weis, wovon ich rede, denn in unserem Unternehmen gibt es an die 100 Services und gut 10 Clients, die damit arbeiten. Mit einer Schnittstelle wie hier beschrieben, würde es hier viele, viele Probleme weniger geben.

    Was aber sehr wichtig ist: SOAP, RPC und ähnliche Ansätze lassen sich niemals 1:1 in RESTful/HATEOS "übersetzen". Das sind unterschiedliche Ansätze, die unterschiedlich funktionieren und unterschiedliche Vor- und Nachteile haben. Wer einen SOAP-Service 1:1 mit REST abbilden will, der kann sich den Aufwand auch direkt sparen, weil damit lediglich Aufwand erzeugt wird.

  6. PS2016-12-14 17:44:07

    Roland "Glatzemann" Rosenkranz: "Entscheidet sich nun jemand, daß man bei einem Druckobjekt ein neues Feld gebrauchen könnte um beispielsweise zusätzlich "Stickereien" anbieten zu können, so kann dies einfach in besagtes "Druckobjekt" hinzugenommen werden. Mit SOAP, RPC, EJB oder ähnlichen hätte ich jetzt schon ein mittelschweres Problem."

    Mit SOAP und EJBs hatte ich auch schon entsprechende Probleme. Allerdings habe ich dort auch nur sehr wenig Erfahrung und möchte meine Hand nicht dafür ins Feuer legen, dass man Schnittstellen-Erweiterungen wie das abwärtskompatible Hinzufügen von optionalen Feldern nicht doch irgendwie umsetzen kann.

    RPC ist ja nicht gleich RPC. Wie kompliziert das Hinzufügen von optionalen Feldern zu existierenden Objekten ist, wird von der RPC-Variante abhängen (CORBA, XML-RPC, GRPC, Web-RPC, ...). Wenn ich hier Web-RPC schreibe, meine ich REST ohne HATEOAS.

    Mit Web-RPC Schnittstellen habe ich bereits viel gearbeitet. Das Hinzufügen von optionalen Feldern war dort nie ein Problem.

    PS: "Bei jeder API-Änderung, die über das Verändern von URIs hinaus geht, muss man trotzdem die Clients anpassen."

    Roland "Glatzemann" Rosenkranz: "Der Client kennt dieses Feld nicht, wird es nicht befüllen und es gibt keinerlei Probleme. Ich kann weiterhin über meinen Druckobjekt-Client Druckaufträge einstellen."

    OK, an dieser Stelle war ich nicht genau genug. Es ist korrekt, dass ich nicht bei optionalen Erweiterungen der API den Client anpassen MUSS. Wie bereits geschrieben, ist dies allerdings auch kein exklusiver Vorteil von REST mit HATEOAS, sondern kann von einfachen Web-RPC Schnittstellen ebenfalls erfüllt werden. Wenn mein Client die neuen Felder allerdings unterstützen soll, MUSS ich den Client anpassen. HATEOAS hilft mir dabei nicht.

    Einen möglichen Einsatz von HATEOAS sehe ich inzwischen bei Fällen, in denen dem Client nicht immer klar ist, welche Aktionen er ausführen darf (Beispielsweise, wenn es Benutzer mit unterschiedlichen Berechtigungen gibt). In solchen Fällen ist die Angabe von möglichen Aktionen über Links an der jeweiligen Ressource sicherlich wünschenswert. Allerdings müssen diese möglichen Aktionen dem Client eben bereits bei der Implementierung bekannt sein. Er wüsste ansonsten nicht, wo die Werte für die Parameter der Aktionen herkommen sollen und wie die Aktion dem Benutzer präsentiert werden soll.

    HATEOAS bleibt für mich eher optional und es sollte immer abgewägt werden, ob die Umsetzung wirklich einen Mehrwert bietet.

    Zweiter Versuch: Mit der Zitat-Funktion komme ich hier noch nicht ganz klar. Mein Kommentar von 17:32:52 kann hier gelöscht werden...

  7. Pascal Bahl2017-10-23 21:36:35

    Guten Abend,

    Eine Notiz am Rande für jene, die sich wundern, wie man denn bitte Informationen über Formulare und deren Validation anbieten soll:

    https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2

    Mich wundert der (wenn auch schon ältere) Hatespeech über Informationen, die unter HATEOAS-Bedingungen angeboten werden können.

    Ich finde den Artikel sehr gut und er hat mir eine Frage beantwortet, die ich zu finden gehofft habe.

    Evtl. könnte der Autor noch Stellung zu den verschiedenen HTTP Methoden nehmen und eine wenig Licht ins dunkle bringen.

    (Ich habe nicht nachgeschaut, ob es neuere Artikel gibt, die diese Themen behandeln. Ich hinterlasse dies nur, falls andere, wie ich, auf diesen Artikel stoßen und in den Kommentaren nach nach Anregung suchen.)

    Die volle Implementierung mit tieferen Informationsgehalt ist möglich und liegt euch bereits durch euren Web Browser in der Hand. :)

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.