Das Anstoßen eines Prozesses über den Aufruf eines Webservices ist an sich ein recht gewöhnliches Problem. SOAP ist hier unser Freund, aber besonders im mobilen Bereich möchte man in der Entwicklung gern leichtgewichtig bleiben und sich kein SOAP-Framework an Bord holen. REST bietet hier den gewünschten leichtgewichtigen Ansatz, doch wie kommt jetzt eine ressourcenorientierte Welt mit einer prozessorientierten Sicht zusammen?
Ein Prozess ist auch nur eine Ressource
Diese einfache, aber doch geniale Idee stammt leider nicht von mir und ich würde hier gern Kudos an den „Erfinder“ geben, aber die Inspirationsquelle ist nicht mehr auffindbar in den unendlichen Weiten meiner Notizen. (Oder sind es unendlich weite Löcher?)
Aber zurück zum Thema. Prozesse sind also auch nur Ressourcen, aber wie geht das jetzt im Detail?
Nun, es ist ganz einfach, wie die meisten guten Dinge auch. Man kommt sehr schnell darauf, wenn man sich anschaut, was eigentlich passiert, wenn man einen Prozess anstößt:
- Mit Hilfe einer Prozessverwaltung werden Prozessinstanzen erzeugt
- Der Zustand einer Instanz kann ausgelesen werden
- Eine Instanz kann von außen verändert werden
Wir sehen hier das typische CRUD-Schema. Ok, ohne das „D“, aber daran soll es nicht scheitern. Also, was wir brauchen, ist eine Ressource, mit deren Hilfe wir Prozessinstanzen erzeugen können. Und wir benötigen je Prozessinstanz eine weitere Ressource, um diese manipulieren zu können.
Und das Ganze schauen wir uns jetzt mal am Beispiel einer Benutzerregistrierung an. Bei dieser soll nicht nur der neue Benutzer angelegt werden, sondern es sollen auch seine Postadresse und seine Bankverbindung verifiziert werden. Nehmen wir zudem einfach mal an, dass dabei noch furchtbar viele andere Dinge passieren und dies außerordentlich lang dauert, weswegen wir nicht auf eine synchrone Antwort warten wollen. Wir stoßen stattdessen einen Prozess an, der uns am Ende ein Ergebnis liefern wird.
Die Prozessverwaltungs-Ressource
Über diese Ressource werden, wie der Name erahnen lässt, neue Prozessinstanzen erzeugt. Aber nicht irgendwelche, sondern nur Instanzen von einem bestimmten Prozess! Ein anderer Prozess würde eine andere Ressource erfordern. Wir könnten sie jetzt Benutzerregistrierungsprozessverwaltung-Ressource nennen und entsprechend würde ihre URI aussehen. Muss aber nicht sein. BenutzerReg ist auch ein guter Name.
Der Aufruf erfolgt, wie bei gutem REST üblich, mit einem POST-Request, der als Payload den initialen Zustand der neuen Instanz enthält. Als Antwort soll man den Status 201 Created und einen Location-Header, der auf die neue Prozessinstanz-Ressource verweist, erhalten. Ein PUT würde man übrigens dann verwenden, wenn einem die URI der neuen Instanz schon bekannt wäre.
Für das gewählte Beispiel sollen die Postadresse und die Bankverbindung als initialer Zustand übergeben werden. Die einzelnen Prozessinstanzen erhalten eine UUID zur Identifizierung. Das sieht dann wie folgt aus:
HTTP Request
POST /Prozess/BenutzerReg <Benutzer> <!-- hier stehen Postadresse und Bankverbindung --> </Benutzer>
HTTP Response
201 Created Location: /Prozess/BenutzerReg/f4081a9c-75be-4a54-b278-55218b97ff56
Die Prozessinstanz-Ressource
Nachdem wir die neue Instanz erzeugt und ihre URI aus dem Location-Header gefischt haben, sollten wir sie auch benutzen.
Die Ressource ist übrigens nicht ganz grundlos als eine Subressource von der Prozessverwaltung modelliert, denn Prozess und Instanz sind untrennbar verbunden. Wird der Prozess gelöscht, hören auch seine Instanzen auf zu existieren. Das heißt, auch die URIs verlieren an Gültigkeit.
Aber nun zu der Prozessinstanz mit dem wohlklingenden Namen f4081a9c-75be-4a54-b278-55218b97ff56. Schauen wir doch mal, wie es ihr so geht:
HTTP Request
GET /Prozess/BenutzerReg/f4081a9c-75be-4a54-b278-55218b97ff56
HTTP Response
200 OK Content-Type: application/xml; charset=UTF-8 <BenutzerReg> <Status>LÖPT</Status> <Benutzer> <!-- hier stehen Postadresse und Bankverbindung --> </Benutzer> </BenutzerReg>
Die Rückgabe besteht in diesem Fall aus einer Repräsentation der Prozessinstanz und liefert uns Informationen über ihren Zustand und ihre Parametrisierung. Was hier genau zurückgegeben wird, liegt zwar grundsätzlich im eigenen Ermessen, die Parametrisierung sollte aber schon enthalten sein. Warum? Das sehen wir gleich. In diesem Fall reicht es uns jedenfalls, zu wissen, dass die Instanz noch LÖPT . Viel wichtiger ist es hier, den Zeichensatz im Content-Type-Header anzugeben, da das „Ö“ sonst Probleme bereiten kann ;)
Dieser Aufruf wird übrigens vom Client gesendet, schließlich ist er (bzw. sein User) derjenige, der am meisten Interesse am erfolgreichen Ausgang des Durchlaufs hat. Man könnte hier, um nicht ständig pollen zu müssen, auch mit Benachrichtigungen arbeiten und dem Client alle Zustandsänderungen mitteilen. Dazu kann man z.B. WebSockets oder die Hausmittel von Android oder iOS benutzen.
Benutzerinteraktion
Aber nun ist es leider nicht immer so, das automatisierte Prozesse immer vollautomatisch durchlaufen. Nehmen wir mal, dass die übergebenen Daten fehlerhaft sind, dass zum Beispiel die Straße in den Adressdaten fehlt. In diesem Fall möchte man den Benutzer darauf hinweisen und ihn bitten, die fehlenden Daten zu ergänzen. Interaktion ist also gefragt!
Für unser Beispiel nehmen wir mal an, dass eine Fehlermeldung zurück kommt, die klipp und klar sagt, dass der Straßenname fehlt. Der Client würde in diesem Fall den User freundlich, aber bestimmt auf seine Unterlassungstat hinweisen und den Straßennamen einfordern. Sobald er diesen erhalten hat … ja was ist dann überhaupt? Naja, wir ändern ihn einfach.
Die aktualisierten Adressdaten des Benutzers werden dem Prozess per PUT übermittelt. Außerdem sagen wir ihm auf diese Art und Weise auch ganz nebenbei, dass er doch bitte weiterlaufen soll, indem wir den Status wieder auf LÖPT setzen. Es werden übrigens alle Daten erneut übermittelt, sonst müsste der Prozess raten, ob sie gelöscht wurden oder unverändert sind. Man kann es natürlich auch anders machen, aber dann müsste man sich etwas ausdenken, um die eigene Absicht zu übertragen – und wir wollen das Interface ja nicht unnötig verkomplizieren. So etwas trägt nur zur Fehlerquote bei, erhöht den Implementierungs- und Testaufwand und lohnt sich für eine Handvoll Bytes in der Regel nicht.
HTTP Request
PUT /Prozess/BenutzerReg/f4081a9c-75be-4a54-b278-55218b97ff56 <BenutzerReg> <Status>LÖPT</Status> <Benutzer> <!-- hier stehen aktualisierte Postadresse und Bankverbindung --> </Benutzer> </BenutzerReg>
HTTP Response
200 OK
Prozessabbruch
Nun nehmen wir mal an, unser Prozess soll vorzeitig beendet werden. Könnte ja sein, dass der User des langen Wartens überdrüssig geworden ist und er sich eine bessere Freizeitgestaltung vorstellen kann. HTTP kennt leider keinen CANCEL-Befehl, also müssen wir auch hier etwas umdenken.
Zwei Möglichkeiten bieten sich an:
- Wir setzen den Status via PUT auf ABBRUCH und gönnen der Instanz eine ordentliche Beendigung
- oder wir senden ein DELETE, um unsere Absicht kenntlich zu machen.
Ersteres würde die Instanz unter ihrer bekannten URI bestehen lassen, letzteres sie löschen. Für das PUT müssen wir aber den kompletten Zustand der Instanz übertragen, weil es inkonsistent wäre, einmal ein PUT mit allen Daten zu verlangen und ein anderes Mal nicht. Andererseits ist genau dies in diesem Fall tatsächlich etwas unhandlich. Ein DELETE wäre sicherlich einfacher und man könnte Anfragen an die gelöschte Ressource mit 210 Gone quittieren, um anzuzeigen, dass die URI zumindest früher einmal gültig war. Ich persönlich neige doch eher zur PUT-Variante, die wie folgt aussieht:
HTTP Request
PUT /Prozess/BenutzerReg/f4081a9c-75be-4a54-b278-55218b97ff56 <BenutzerReg> <Status>ABBRUCH</Status> <Benutzer> <!-- hier stehen aktualisierte Postadresse und Bankverbindung --> </Benutzer> </BenutzerReg>
HTTP Response
200 OK
Fazit
Ich hoffe, dass ich zeigen konnte, das REST und Prozesse zusammenpassen können, und gleichzeitig einen – wenn auch nur groben – Wegweiser geschaffen habe, um diese Welten zu verheiraten.