REST ist wie Japanisch — nur anders

Von manchen Dingen gibt es einfach nur endlich viele. Von Pronomen beispielsweise. Ich, du, er, sie, es, wir, ihr, sie. Das sind die Personalpronomen. Gut, man kann diese noch deklinieren, aber dann ist auch schon Schluss. Niemand würde auf die Idee kommen, ein neues Personalpronomen zu erfinden. Es gibt einfach nur endlich viele davon. Keine Chance, das zu ändern. Anders ist es beispielsweise mit Verben. Davon entstehen ständig neue. „Simsen“, „googlen“, „chillen“, „refactoren“… Es gibt immer wieder Neologismen. Niemand würde auf die Idee kommen, zu behaupten, man hätte bereits alle Verben erfunden. Man sagt, Pronomen sind eine „Closed Word Class“ und Verben sind „Open Word Class“ [1]. Die Unterscheidung braucht man mitunter, um zu bestimmen, welche Wörter man in englischen Überschriften groß schreibt.

Jetzt kommt das Interessante: Dass Pronomen endlich sind und Verben nicht, ist für uns zwar einigermaßen klar und intuitiv, aber es kann auch anders sein. Ich kann zwar kein Japanisch, aber zumindest laut Wikipedia ist es dort genau umgekehrt. Dort gibt es nur endlich viele Verben, aber potenziell unendlich viele Pronomen. Beide Varianten funktionieren also offensichtlich. Zumindest gehe ich davon aus, dass sich Japaner untereinander verstehen.

Wenn ich Japanisch lernen wollte, so müsste ich also erst einmal die Denkweise der japanischen Grammatik lernen. Die romanischen und germanischen Sprachen, die wir so typischerweise kennen, funktionieren alle ähnlich. Egal ob Deutsch, Englisch, Französisch oder Italienisch: Die Vokabeln unterscheiden sich und auch die Grammatik ist anders, aber die grundlegende Denkweise: grammatische Formen, Kasus, Zeiten, Pronomen, Deklination und Konjugation… Die grundlegenden Konstruktionsbausteine der Sprachen sind sehr ähnlich. Das ist aber keine Selbstverständlichkeit. Andere Sprachen können durchaus auch anders funktionieren. Und beim Japanischen und vermutlich noch vielen anderen (asiatischen, etc.) Sprachen ist das wohl so. Unser Wissen, wie man Verben konjugiert, hilft beim Lernen von Japanisch nicht. Im Französischen konjugiert man anders als im italienischen. Aber trotzdem sind Verben immer noch von Numerus und Person abhängig. Japanisch ist hier ganz anders.

Ähnliche Effekte lassen sich auch in der Softwareentwicklung beobachten. Sprachen wie Java, C++ oder Delphi haben im Detail große Unterschiede. Aber Konzepte sind ähnlich. Und es gibt Sprachen, die vollkommen anders sind. In Haskell beispielsweise gibt es noch nichtmal Variablen.

Aber die Analogie lässt sich noch weiter treiben: Auch in Programmiersprachen und Technologien gibt es endliche und erweiterbare Strukturen — Closed Classes und Open Classes. Typische Programmiersprachen, Technologien und Architekturen gehen einen ähnlichen Weg wie das Englische (und diverse andere, wenn nicht sogar alle europäische Sprachen): Es gibt potenziell unbegrenzt viele Verben bzw. Methoden. Aber muss das so sein? Könnte man nicht… so wie im Japanischen? Kann man. Und das tut REST. Dort gibt es auch nur endlich viele Verben. Aber gucken wir uns das einmal genauer an…

REST ist quasi eine Art Grammatik für Kommunikation in verteilten Systemen. Und der wichtige Punkt, den man verstehen muss: Diese Grammatik erfordert eine andere Denkweise. Genauso wie Japanisch. In REST gibt es, ähnlich wie im Japanischen, nur eine begrenzte Anzahl Verben. Es gibt sogar nur sehr wenige davon: GET, PUT, POST und DELETE. Daneben gibt es noch HEAD und OPTIONS, die aber keine sonderliche Rolle spielen. Prinzipiell lassen sich — in sehr begrenztem Maße — auch die Verben erweitern und so findet das Verb PATCH langsam aber sicher immer mehr Verbreitung. Das Erweitern von HTTP um eigene Verben ist (wenn an es richtig machen will) kompliziert und sowieso ein Spezialfall. Mit PATCH will ich eine einzige Ausnahme machen, weil das eine wirklich sinnvolle Erweiterung ist. Zudem ist PATCH seit 2010 ein „Proposed Standard“ und damit auf einer Ebene mit Technologien wie LDAP und IMAP. Und diesen kann man nun wirklich nicht vorwerfen, sie steckten noch in den Kinderschuhen. Bis an einem RFC mal das Wort „Internet Standard“ prangt, ist es ein eeeecht weiter Weg.

Wir können jetzt also davon ausgehen, dass es nur die bekannten vier bzw. fünf Verben gibt. Nicht mehr und nicht weniger. GET, PUT, POST, DELETE und ausnahmsweise PATCH, das sind die Verben. Und man wendet sie auf Ressourcen an:

1
2
3
4
5
6
GET /customers
GET /customers/12345
PUT /customers/12345 {... new data for existing customer...}
PATCH /customers/12345 {... new data for existing customer...}
POST /customers {... new customer data ...}
DELETE /customers/12345

[2]

Das, was einem dabei zuerst einfällt, ist wohl, dass das genauso aussieht, wie die typischen CRUD-Operationen: Create, Read, Update, Delete. Man ist versucht, direkte Entsprechungen zu sehen, aber das stimmt nur bedingt. So wie Haskell-Werte und -Funktionen auf den ersten Blick so ähnlich aussehen, wie Variablen und Methoden in Java. Im Detail gibt es dann doch Unterschiede.

So kann man beispielsweise auch mit PUT neue Ressourcen erstellen. Außerdem sind dadurch, dass REST-Ressourcen oft mehr sind, als direkte Entsprechungen der Datenbank-Entities, durchaus komplexere Geschäftslogiken darstellen, die weit über das hinausgehen, was man üblicherweise unter CRUD versteht. Eine Rest-Ressource kann sogar ein Prozess sein. Ein POST auf die Liste der Prozesse startet einen neuen Prozess (d.h. erzeugt eine neue Prozess-Ressource), etc.

Aber das ist nicht noch nicht alles. Die HTTP-Verben lassen sich anhand zweier Eigenschaften kategorisieren: Safety und Idempotenz:

Method Idempotent Safe
GET
PUT
DELETE
POST
PATCH

Eine Methode ist idempotent, wenn man sie mehrmals hintereinander ausführen kann, ohne dass sich dabei das Resultat ändert. „inkrementieren um 1“ ist demnach nicht idempotent. „Setzen auf 42“ schon. Eine Methode ist darüber hinaus „safe“, wenn man sie einfach so ausführen kann, ohne dass das (ggf. nachteilige) Seiteneffekte haben darf. Es muss erlaubt sein, zu jeder Zeit, ohne groß darüber nachdenken zu müssen, GET auszuführen. Die Ressource darf sich dadurch nicht ändern. Gleichwohl darf sich beispielsweise ein Zugriffszähler ändern, das ist kein Problem. Ganz Seiteneffektfrei muss GET nicht sein. Aber es darf nicht dazu kommen, dass ich aus technischen oder fachlichen Gründen zu einem Zeitpunkt kein GET machen darf. Das muss immer erlaubt sein.

Die Semantik der HTTP-Verben hängt dabei davon ab, ob die Ressource, auf die sie angewendet werden eine listenartige oder eine eiinzelstückartige ist.

1
/articles

könnte die Liste der Blog-Artikel repräsentieren.

1
/articlkes/42

wäre dann der Artikel mit der ID 42.

Verb Einzelressource Listenressource
GET Einzelressource lesen Komplette Liste lesen; ggf. Linkliste auf die Elemente
PUT Einzelressource ersetzen Komplette Liste ersetzen
DELETE Einzelressource löschen Komplette Liste löschen
POST (ggf. Subressource erzeugen) Neues Listenelement erzeugen
PATCH Einzelressource nach gegebenem Muster verändern Komplette Liste nach gegebenem Muster verändern

GET und DELETE sollten intuitiv klar sein. Bei POST ist zu beachten, dass man damit generell Subressourcen (z.B. Listenelemente) erzeugt. PUT ersetzt immer die komplette Ressource. Dadurch ist es besonders einfach, sie idempotent zu machen. Dafür muss man immer eine vollständige Repräsentation der Ressource übertragen, was mitunter recht unbequem sein kann und bei großen Ressourcen auch das Netzwerk belastet. PATCH ist hier flexibler, indem es Teil-Updates erlaubt. Der Body einer PATCH-Ressource kann dabei eine beliebige Beschreibung der Änderung (quasi ein „Diff“) enthalten. Der Standard schreibt nicht vor, wie das auszusehen hat, weshalb das sehr flexibel ist. Der Preis der Flexibilität ist, dass Idempotenz bei PATCH nicht vorgeschrieben ist. Eine PATCH-Operation muss also nicht idempotent sein. Gleichwohl darf sie es aber natürlich und häufig ist es auch sehr sinnvoll sie so zu gestalten, dass sie idempotent ist.

Alles in allem ist jedenfalls wichtig, dass die Ressourcen keine Methoden darstellen. Die Verben sind die vier bzw. fünf genannten und wenn man Operationen hat, die mehr sind als CRUD, muss man das anders ausdrücken. Die URL zum Verb zu machen ist keine Lösung: Wenn man RPC in irgend einer Form (RMI, SOAP, DCOM, CORBA, …) gewohnt ist, ist man ja versucht, Methoden auf URLs abzubilden:

1
2
3
4
POST /employees/raiseSalary?percent=10
POST /database/startOptimization
PUT /groups/rest-haters/join
PUT /articles/42/publish

oder noch schlimmer:

1
POST /rest/method/paramname/paramvalue

beispielsweise

1
POST /rest/helloWorld/name/Alice

Die schlimmste Variante hat wohl flickr für sich entdeckt:

1
GET https://api.flickr.com/services/rest/?method=flickr.test.echo&name=value

flickr nennt das „REST“. Ich nenne das „das Prinzip nicht verstanden“.

Aber wie macht man es besser? Gucken wir uns das mal der Reihe nach an. Das Hello-World-Beispiel würde man ja typischerweise über eine Methode

1
greet(String name);

umsetzen. Nun ist GREET keines unserer HTTP-Verben. Und wir sollten auch das Verb nicht in die Ressource einkodieren. Stattdessen müssen wir den Sachverhalt anders ausdrücken: Wir definieren eine Ressource

1
/greetings

, die die (potenziell unendlich große) Liste der Grüße darstellt. Über

1
/greetings/Alice

referenzieren wir den Gruß an Alice. Und per GET können wir uns den geben lassen. Die Operation ist idempotent und safe und sie gibt ein Ergebnis zurück:

1
GET /greetings/Alice

Weiter gehts: Angenommen wir wollen einen Blog-Artikel veröffentlichen.

1
PUT /articles/42/publish

ist nicht ganz richtig. publish ist semantisch ein Verb. Wir sollten das also nicht in die URL einkodieren. Vielmehr müssen wir eine Ressource definieren. Beispielsweise kann ein Artikel eine Subressource „published“ haben, die angibt, ob der Artikel veröffentlicht wurde. Das kann ja durchaus ein Boolean sein. Ein GET liefert (ggf. json-, XML- oder sonstwie kodiert) zurück, ob der Artikel veröffentlicht wurde. Per PUT kann man das auf „true“ setzen:

1
PUT /articles/42/published true

Alternativ könnte man auch das Veröffentlichungsdatum setzen:

1
PUT /articles/42/publicationDate "2014-11-04T17:48:35Z"

oder

1
PUT /articles/42/publicationDate "now"

und der Server bestimmt die aktuelle Uhrzeit selbst. Der Vorteil an der Schnittstelle ist, dass man so auch direkt eine Möglichkeit hätte, den Artikel automatisiert in der Zukunft online zu stellen oder rück zu datieren.

Einer Gruppe beitreten kann man folgendermaßen:

1
POST /groups/rest-haters {"name":"Alice"}

Ein POST auf die Liste der Gruppenmitglieder erzeugt ein neues Gruppenmitglied (bzw. eine neue Subressource in der Liste).

So weit so einfach. Interessant ist jetzt die Frage, wie man Aktionen ausführen kann. Werte setzen und lesen ist das eine. Aber wie sieht es damit aus, wenn wir beispielsweise eine Datenbank haben, die regelmäßig die Indizes optimieren muss oder ähnliches. Wir wollen diese Aktion anstoßen. Wie beschreiben wir das nun in REST? Klingt erstmal so, als würde REST hier gar nicht so recht passen, aber effektiv haben wir hier sogar mehrere Möglichkeiten zur Auswahl:

1
PUT /database/optimized true

Über die Ressource

1
/database/optimized

können wir abfragen, ob die DB meint, eine Optimierung sei mal wieder notwendig. Und ein PUT auf true führt die Aktion dann aus. Das Ganze bleibt so lange auf true bis die DB wieder unordentlich ist und aufgeräumt werden muss.

Alternativ können wir auch wieder den Trick mit dem Startzeitpunkt anwenden:

1
2
3
4
PUT /database/nextOptimization "now"
PUT /database/nextOptimization "tonight"
PUT /database/nextOptimization "next Thursday"
PUT /database/nextOptimization "2014-11-04T03:30:00Z"

Der Wert verbleibt auf dem gesetzten so lange wie die Operation läuft. Ist sie abgearbeitet, verschwindet die Ressource. Die PUT-Operation ist für die Dauer der Optimierung idempotent, was durchaus in Ordnung ist. Man kann nicht erwarten, dass die Zeit stehen bleibt und ein PUT nächste Woche immer noch als Wiederholung erkannt wird. Wenn das aufrufende System aber für so lange ausfällt, hat man eh andere Sorgen, sodass das erneute Optimieren der Datenbank das geringere Problem darstellt. Hat man strengere Anforderungen, bei denen das erneute Ausführen auch bei langen Ausfällen unbedingt vermieden werden muss, muss man anders rangehen. Eine Möglichkeit wäre im vorliegenden Fall einfach statt den Zeitpunkt der nächsten Optimierung zur Ressource zu machen, den Zeitpunkt der letzten zu nehmen:

1
PUT /database/lastOptimization "2014-11-04T19:56:00Z"

Der Client bestimmt die aktuelle Uhrzeit, macht ein PUT und der Server speichert den Zeitstempel und legt mit der Optimierung los. Wiederholt der Client die Anfrage (die sich beispielsweise in einer Message-Queue befindet), sieht der Server, dass der angegebene Zeitstempel dem hinterlegten entspricht (oder ggf. sogar älter ist). Der Server wird das mit einem „OK, hab ich gemacht“ quittieren und alles ist gut. Selbst, wenn die Queue für eine Woche hängt, wird die Aktion immer genau einmal durchgeführt.

Angenommen unsere Datenbankoptimierung dauert so lange, dass man sie ggf. abbrechen können oder zumindest deren Fortschritt erfragen möchte. In dem Fall kann man auch die Optimierung als solche zur Ressource erklären:

1
POST /database/optimizations {"level":"optimizeThoroughly", "startTime":"2014-11-05T03:30:00Z"}

Das würde eine neue Optimierungsoperation erzeugen, die zum angegebenen Zeitpunkt losläuft (und ggf. sehr lange dauert). Der Server liefert dann eine Antwort mit einem Location-Header:

1
Location: http://.../database/optimizations/e01170a7-9533-4232-af3a-2fc6fc727fd7

Für die Optimierungsoperation existiert nun eine eigene Ressource, auf der der Client wieder arbeiten kann. GET liefert den aktuellen Status (Startzeit, Zustand, Ersteller, Fortschritt, aufgetretene Fehler, etc.), PUT/PATCH ändert den Auftrag, DELETE löscht ihn bzw. bricht ihn ab, etc.

Das letzte Beispiel wäre eine Gehaltserhöhung per Gießkanne: Alle kriegen 10% mehr. RPC wäre folgendes:

1
POST /employees/raiseSalary?percent=10

Für echtes REST müssen wir das ein bisschen anders machen:

1
PATCH /employees {"salary":"+10%"}

Das Ganze ist sogar so flexibel, dass man die Operation auch auf Teil-Listen machen kann. Über einen Query-Parameter filtert man die Liste und macht darauf das PATCH:

1
PATCH /employees?role=developer {"salary":"+10%"}

So erhalten nur Entwickler die Gehaltserhöhung.

So. Und jetzt kommt der Teil, der mir bei vielen REST-Tutorials fehlt. Die Beantwortung der einen Frage, die sich alle, die REST machen oder lernen wollen, stellen sollten: Warum das Ganze? Was ist so schlimm daran, wenn ich mich nicht an das ganze Zeug hier halte? Was ist so schlimm daran, wenn ich Methoden in URLs kodiere, immer GET verwende, GET nicht safe und PUT nicht idempotent mache, etc.?

Das erste, was einem da so einfällt ist „das macht man so nicht„. Auf den ersten Blick sieht das aus wie ein ziemlich schwaches Argument. „Das macht man nicht“ sagt man zu Kindern, nicht zu einem ausgewachsenen Entwickler…. oder? Genauer betrachtet ist an dem Argument nämlich doch etwas dran. Nicht dass es das einzige Argument wäre. Zu den anderen Argumenten kommen wir gleich noch. Aber erst mal das hier. Es ist durchaus sinnvoll, eine Technoloigie so zu verwenden, wie sie gedacht ist. Wenn man mal die Denkweise von REST verstanden hat, ist es i.d.R. recht leicht, andere REST-Service zu verstehen. Zumindest, wenn diese sich an die Regeln halten. Etwas ist leicht zu verstehen und leicht zu benutzen, wenn es möglichst wenige Abweichungen von der Norm hat. Das, was anders ist, muss man neu lernen und wer sich an die Spielregeln hält, wird leicht verstanden. Man kann diesem Prinzip sogar einen Namen geben: Uniformity Principle. Eine Lösung ist gut, wenn sie gleichförmig ist.

Umgekehrt muss jeder, der sich nicht an die Spielregeln hält, und etwas anders macht, die Frage gefallen lassen, warum man nun davon abweicht. Im Einzelfall mag es durchaus Gründe geben, sich mal nicht an alles zu halten. Aber man sollte auf die Frage nach dem Warum eine passende Antwort wissen.

Kommen wir nun zu den anderen Gründen: Vieles in REST dreht sich um Skalierbarkeit in verteilten Systemen. Das Web skaliert offensichtlich auf Weltniveau. Aber wie geht das? Skalierbarkeit wird vor allem durch Replikation erreicht. Im Web passiert das durch Client-Caches, Client-side Proxies, ReverseProxies (als LoadBalancer und/oder als Cache), über Cluster, Sharding und ähnliches. Sehr viele der Eigenschaften von REST drehen sich genau darum, diese Skalierbarkeit möglich zu machen.

Die erste Eigenschaft von REST-Services ist, dass sie stateless sind. Das ist mittlerweile fast nicht mehr erwähnenswert und wir haben uns das oben auch gar nicht näher angesehen, weil die Erkenntnis, dass zustandslose Dienste bedeutend besser skalieren als zustandsbehaftete, recht weit verbreitet sein sollte. Wenn ein Service Session-State hat, also die Semantik eines Aufrufs potenziell von vorherigen Aufrufen abhängt, hat das weitreichende Folgen. Zum einen muss der Server diesen Zustand für jeden Client halten. Das braucht Speicher, was für Denial-of-Service-Angriffe ausgenutzt werden kann aber auch ungewollt für Überlast sorgen kann. Viel mehr noch: Möchte man den Service in einem Cluster betreiben, muss sicher gestellt sein, dass ein Client, wenn er einmal angefangen hat, mit Node1 zu reden, auch jeder folgende Aufruf an Node1 des Clusters gerichtet sein muss. Node2 hat den vorherigen Call ja gar nicht erhalten. Um das zu bewerkstelligen, muss der LoadBalancer deutlich komplexer werden. So braucht er auch wiederum Speicher, um das Mapping von Clients auf Cluster-Nodes zu bewerkstelligen (und der Speicher kann auch wieder voll laufen). Außerdem kann man nicht mehr so leicht einen Node aus dem Cluster nehmen und die Zuverlässigkeit sinkt. Alternativ könnte man auch verlangen, dass sich die einzelnen Nodes eines Clusters den Speicher für den Session State teilen bzw. diesen synchronisieren. Auch das ist wieder aufwendig, fehleranfällig und unnötig komplex. Besser also man macht Services stateless.

Weiter: Was ist so schlimm daran, wenn GET nicht safe ist? Proxies oder auch Clients möchten manchmal Prefetching betreiben, um performance zu optimieren. Und Caches können beliebige Inhalte zu beliebigen Zeitpunkten aus dem Speicher werfen und dann irgendwann neu anfordern wollen. Deshalb muss ein GET immer safe sein. Macht man GET nicht safe, bedeutet das quasi einen Verzicht auf jegliche Möglichkeit des Cachings. Keine gute Idee.

Was, wenn PUT nicht idempotent ist? Eine Eigenschaft von verteilten Systemen ist, dass sie verteilt sind. Zwischendurch kann einfach mal kurz die Netzwerkverbindung weg sein. Vielleicht hat jemand das Kabel aus der Wand gezogen oder jemand spielt an der Firewall oder ein Bagger gräbt dort, wo er nicht soll. Kann alles vorkommen und in diesen Fällen gehen Nachrichten verloren. Der Sender schickt etwas ab und erhält keine Antwort. Bedeutet das, dass die Nachricht verloren gegangen ist? Oder bedeutet das, dass die Nachricht zwar verarbeitet wurde, aber die Antwort ins Nirvana entfleucht ist? Der Aufrufer kann das nicht wissen. Die Lösung von REST ist hier, dass man einen PUT-Request einfach nochmal machen darf. Wir sind idempotent, d.h. eine Wiederholung wird nichts Negatives bewirken. Halten wir uns hier nicht dran, kriegen wir Probleme, wenn das Netzwerk mal wegbricht.

Was ist, wenn wir die verschiedenen HTTP-Verben nicht nutzen, sondern alles über POST machen und im Body den eigentlichen Request mitgeben? So wie SOAP? Dann kann man nicht gescheit cachen (bzw. man braucht einen ziemlich komplizierten Cache). Das schöne an REST ist doch, dass man einfach einen HTTP-Proxy von der Stange nehmen kann. Caching, Tools zum Debugging, Profiling, etc. Alles das existiert schon und auf dem Niveau, dass es mit dem Web fertig wird. Ein HTTP cache weiß, dass er Antworten von GET-Requests cachen darf und kann auch die standardisiertn Cache-Control-Header-Felder auswerten, etc. Das muss man nicht neu erfinden. Außer man meint, nicht nicht an den Standard halten müssen. Außerdem brauchen wir nicht zu erwarten, dass sich aus den HTTP-Logfiles irgend etwas Sinnvolles ableiten lässt, wenn wir nur eine URL haben. Die Nachvollziehbarkeit ist nicht unbedingt gegeben und das Debugging erschwert. Außerdem nutzen wir dann ja die erklärte Idempotenz nicht (mit den genannten Nachteilen).

Es ist also schon ganz gut, wenn man sich zumindest bemüht, REST zu verstehen und sich ein bisschen an die Grammatik hält. Wir haben ja auch gelernt, Methoden sauber zu benennen. Dann sollten wir auch unsere APIs sauber entwerfen. Und das ist durchaus schaffbar. Denn REST ist wie Japanisch. Rest ist nicht schwer [3]. Es ist nur anders.

[1] Die Unterscheidung gibts mit Sicherheit auch im Deutschen, aber ich bin jetzt zu faul, das zu recherchieren — zumal ich hier ja eigentlich etwas über REST sagen will.
[2] Im folgenden deute ich den Body eines HTTP-Requests immer in der gleichen Zeile an, obwohl dieser natürlich durch eine Leerzeile getrennt unter der Zeile mit Verb+Ressource folgt. Diese Details übernimmt in aller Regel eh eine Library und mir geht es hier um ganz andere Dinge.
[3] Darüber wie schwer es ist, Japanisch zu lernen, will ich mir kein Urteil erlauben.

3 Kommentare



  1. Lustige Veranschaulichung. Danke. Ich ziehe meinen Hut vor RSA!


  2. Hallo Claudio!

    Freut mich, dass es dir gefällt!

    Viele Grüße

    Christian

    P.S.: RSA?

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert