Objekt mit Statusübergang

Vor einiger Zeit habe ich für eie in Java entwickelte Web-Anwendung ein Objekt erstellen müssen, das erst über einen Genehmigungsprozess nach “Vier Augen Prinzip” tatsächlich Gültigkeit erlangt. Dabei wird durch einen ansonsten nicht weiter autorisierten Benutzer “0” eine Instanz des Objekts angelegt und erst nach der vollständigen Erfassung für eine Entscheidung freigegeben. Danach müssen zwei Entscheider, “1” und “2”, diesen Vorgang zur Überarbeitung zurückweisen, ablehnen oder genehmigen. Diese Prozess wird durch die nachfolgende Skizze vielleicht ein wenig deutlicher:

Statusdiagramm

Um diesen Genehmigungsprozess korrekt abbilden zu können, habe ich in diesem Objekt eine Reihe von Statusfeldern angelegt, die den Objektzustand im Genehmigungsprozess repräsentieren.

Das erste implementierte Modell

Zuerst einmal ist ein Status für den Ersteller zu definieren. Der Objektzustand aus Sicht des Erstellers kann eigentlich nur zwei disjunkte Status einnehmen:

public enum ErstellerStatus {
    OFFEN,
    FREIGEGEBEN;
}

Aus Sicht der Entscheider “1” und “0” ist der Status ein wenig komplizierter. Hier muss auch eine Zurückweisung oder Ablehnung dokumentiert werden.

public enum Entscheiderstatus {
    OFFEN,
    ZURUEKGEWIESEN,
    GENEHMIGT,
    ABGELEHNT;
}

Zusätzlich ist es natürlich auch noch sinnvoll, den eigentlichen Zustand des Objekts innerhalb des Prozesses in einem Typ darzustellen.

public enum Objektstatus {
    IN_ERFASSUNG,
    ERWARTET_ENTSCHEIDUNG_1,
    ERWARTET_ENTSCHEIDUNG_2,
    GENEHMIGT,
    ABGELEHNT;
}

Innerhalb des implementierten Objekts sind diese einzelnen Statusinformationen hinterlegt. Zusätzlich muss natürlich auch das Datum einer Entscheidung und die zur entscheidende Begründung vorgehalten werden um im Fall einer Revision auskunftsfähig zu sein. Damit müssen einige Attribute für die Abbildung dieses Entscheidungsvorgangs angelegt werden.

public class Objekt {
    private enum ErstellerStatus erstellerstatus;
    private Date freigabedatum;
    
    private enum EnscheiderStatus entscheiderstatus_1;
    private Date entscheidungsdatum_1;
    private String begruendung_1;
    
    private enum EntscheiderStatus entscheiderstatus_2;
    private Date entscheidungsdatum_2;
    private String begruendung_2;
}

Der ObjektStatus ist in diesem Objekt nicht als Attribut angelegt, da er transient auf Basis der Einzelstatus berechnet werden kann. Daher wäre das Ablegen dieser Information redundant. Allerdings kann es aus Gründen der Performance — insbesondere bei einer Speicherung in einer relationalen Datenbank — sinnvoll sein, dieses Attribute dennoch anzulegen und bei jedem Update neu zu berechnen.

@Transient
private ObjektStatus getObjektStatus() {
    if (erstellerstatus == ErstellerStatus.OFFEN)
        return ObjektStatus.IN_ERFASSUNG;

    switch (entscheiderstatus_1) {
    case EntscheiderStatus.OFFEN:
        return ObjektStatus.ERWARTET_ENTSCHEIDUNG_1;
    case EntscheiderStatus.ZURUECKGEWIESEN:
        return ObjektStaus.IN_ERFASSUNG;
    case EntscheiderStatus.GENEHMIGT:
        break;
    case EntscheiderStatus.ABGELEHT:
        return ObjektStatus.ABGELEHT;
    }

    switch (entscheiderstatus_2) {
    case EntscheiderStatus.OFFEN:
        return ObjektStatus.ERWARTET_ENTSCHEIDUNG_2;
    case EntscheiderStatus.ZURUECKGEWIESEN:
        return ObjektStaus.ERWARTET_ENTSCHEIDUNG_1;
    case EntscheiderStatus.GENEHMIGT:
        return ObjektStatus.GENEHMIGT;
    case EntscheiderStatus.ABGELEHT:
        return ObjektStatus.ABGELEHT;
    }
}

Diese Berechnung des Workflowstatus funktioniert, aber schön ist das nicht.

Probleme bei der Realisierung einer REST-ähnlichen Schnittstelle

Zu einem Problem wurde das so modellierte Objekt aber in dem Moment, in dem über ein REST-ähnliches API das Fernsteuern dieser Anwendung ermöglicht werden sollte. Auf der Seite des CLients die Abfolge erlaubter Zustandsübergänge modellieren zu müssen und den neu berechneten Zustand in die Objektinstanz verbietet sich offensichtlich (hoffentlich). Während auf den jeweiligen Objektinhalt über die URL /objekte/123 zugegriffen werden kann wurden aus Zeitgründen die Funktionen für das Fortschreiten im Entscheidungsprozess einfach als Sub-URLs implementiert. Für eine Freigabe wird also der URL /objekte/123/freigabe geschrieben. Das Objekt wird so veranlasst, im Workflow fortzufahren. Selbstverständlich wollten ich dennoch auch das HATEOAS Konzept bereitstellen, so dass ich immerhin die jeweils gültigen “Funktionslinks” entsprechend des Objektzustands ermittelt habe.

Ich bin mit Stefan Tilkov bekannt und daher scheue ich mich naturgemäß eine meiner Schnittstellen als “REST API” zu bezeichnen. Mir ist bewusst, wie viel - drücken wir es einmal so aus - konstruktive Kritik ich bei unserem nächsten Treffen empfange dürfte. Daher sichere ich mich grundsätzlich gerne ab und nenne das API nur “REST-ähnlich.” Aus Zeitmangel sind häufig Kompromisse erforderlich, die einer idealen, sauberen Schnittstelle entgegen stehen. Jedoch versuche ich immer darauf zu achten, ob die Probleme an der Schnittstelle eventuell doch auf Designschwächen der Anwendung zurückzuführen sind.

In diesem Fall war genau das der Fall. Die Implementierung der REST-ähnichen Schnittstelle (Sehr geehrter Leser, wurde diese Formulierung bemerkt?) weist tatsächlich auf eine Designschwäche hin: dieser Entscheidungsprozess kann relativ lange dauern und durchaus mehrere Abstimmungsrunden zwischen Erstellern und Entscheidern erfordern. So muss diese Entscheidung nicht unbedingt die ideale Zustandsabfolge durchlaufen.

"0" - "1" - "2" - GENEHMIGT

Es ist auch durchaus möglich, dass bis zu einer endgültigen Entscheidung eine weitaus längere Zustandsabfolge notwendig wird.

"0" - "1" - "0" - "1" - "2" -"1" - "0" - "1" - "2" - "1" - "2" - GENEHMIGT

In den Attributen des Objekts kann jedoch nur die jeweils letzte Entscheidung dokumentiert werden. Alle Zeitpunkte und Begründungen vorangehender Durchläufe gehen verloren und stehen für eine Revision nicht mehr zur Verfügung.

Das Modell nach dem Redesign

Das Problem, das erst durch die zusätzliche Integration einer REST-ähnlichen Schnittstelle sichtbar wurde, hat mich zu einem Redesign meines Datenmodells bewegt. Anstatt die Zustandsdaten im Objekt zu speichern, habe ich ein Collection mit Genehmigungen eingefügt.

public enum Entscheider {
    ERSTELLER,
    ENTSCHEIDER_1,
    ENTSCHEIDER_2;
}

public enum GenehmigungsStatus {
    GENEHMIGT,
    ZURUECKGEWIESEN,
    ABGELEHNT;
}

public class Genehmigung {
    private Entscheider entscheider;
    private GenehmigungsStatus genehmigung;
    private Date entscheidungsdatum;
    private String begruendung;
}

public class Objekt {
    private List<Genehmigung> genehmigungen;
}

Auch jetzt kann, durch Betrachten der jeweils aktuellsten Genehmigung, der korrekte Zustand im Prozess ermittelt werden.

@Transient
private ObjektStatus getObjektStatus() {
    if (genehmigungen.size().isEmpty())
        return ObjektStatus.IN_ERFASSUNG;
    Genehmigung act = genehmigungen.get(genehmigungen.size() - 1);
    if (act.genehmigung == GenehmigungsStatus.ABGELEHNT)
        return ObjectStatus.ABGELEHNT;
    if (act.genehmigung == GenehmigungsStaus.GENEHMIGT) {
        switch (act.entscheider) {
        case ERSTELLER: return ObjektStatus.ERWARTET_ENTSCHEIDUNG_1;
        case ENTSCHEIDER_1: return ObjektStatus.ERWARTET_ENTSCHEIDUNG_2;
        case ENTSCHEIDER_2: return ObjektStatus.GENEHMIGT;
    }
    if (act.genehmigungsstatus == GenehmigungsStatus.ZURUECKGEWIESEN) {
        case ENTSCHEIDER_1: return ObjektStatus.IN_ERFASSUNG;
        case ENTSCHEIDER_2: return ObjektStatus.ERWARTET_ENTSCHEIDUNG_1;
    }
}

Tatsächlich erfolgt die Speicherung dieses ObjektStatus innerhalb des Objekts. So können zur Laufzeit der Anwendung sehr schnell die Instanzen ermittelt werden, die für einen Ersteller oder Entscheider in seiner jeweiligen Rolle relevant sind.

Auf diese Weise ist es auf jeden Fall möglich, eine Genehmigung im Entscheidungsprozess als zusätzliche Sub-Resource dem eigentlichen Objekt hinzufügen. In der Darstellung als Ressource wird dem eigentlichen Objekt ein zusätzlicher Link beigefügt, der auf die Möglichkeit einer Genehmigung hinweist, wenn der jeweilige API-Nutzer über die entsprechende Rechte verfügt.

Beispiel für die Nutzung des API

Der Entscheidungsprozess kann jetzt daher über das REST-ähnliche API wie folgt abgewickelt werden:

Anlegen eines neuen Entscheidungsprozesses

POST /objekte
{
    ...
}

201 CREATED /objekte/123

Lesen des neu erzeugten Objekts

In dem neu erzeugten Objekt ist neben dem “self” Link auch ein weiterer Link der Relation “genehmigung” enthalten, der auf die Möglichkeit hinweist, den Entscheidungsprozess weiter zu führen.

GET /objekte/123

200 OK
{
    status: "IN_ERFASSUNG",
    links: [ { href: "/objekte/123/",
               rel: "self" },
             { href: "/objekte/123/genehmigung/",
               rel: "genehmigung",
               method: "POST" } ]
}

Freigeben durch den Ersteller

Der Ersteller - und nur er - kann den “genehmigung” Link nutzen, um eine neue Genehmigung anzulegen und den Entscheidungsprozess weiterzuführen. Zurückgeliefert wird ein Link auf die neu erzeugte Genehmigungs-Ressource.

POST /objekte/123/genehmigung
{ begruendung: "Erfassungist vollständig",
  genehmigung: "GENEHMIGT"
}

201 CREATED /objekte/123/genehmigung/0

Lesen der Genehmigung

Diese Genehmigung kann nun vom Client eingelesen werden. So erhält der Client wieder ein Attribut der Relation “parent”, der auf den Entscheidungsprozess verweist.

GET /objekte/123/genehmigung/0

200 OK
{   entscheider: "ERFASSER",
    genehmigung: "GENEHMIGT",
    datum: "2013-05-06 23:00",
    begruendung: "Erfassung vollständig",
    parent: "/objekte/123",
    links: [ { href="/objekte/123/genehmigung/0",
               rel="self" },
             { href="/objekte/123/",
               rel="parent" } ]
}

Lesen des geänderten Objekts

Wird nun wieder unser Entscheidungsprozess betrachtet, wird nun der neue Zustand dargestellt. Er enthält die neu erzeugte Genehmigung des Erstellers. Da dieser nun nicht mehr auf das Objekt zugreifen darf, wird kein weiterer Link der Relation “genehmigung” dargestellt.

GET /objekte/123

200 OK
{
    status: "ERWARTET_ENTSCHEIDUNG_1",
    genehmigungen: [ { entscheider: "ERFASSER",
                       genehmigung: "GENEHMIGT",
                       datum: "2013-05-06 23:00",
                       begruendung: "Erfassung vollständig",
                       parent: "/objekte/123",
                       links: [ { href="/objekte/123/genehmigung/0",
                                  rel="self" },
                                { href="/objekte/123/",
                                  rel="parent" } ]
                     } ],
    links: [ { href: "/objekte/123/",
               rel: "self" } ]
}

Zusammenfassung

In diesem Beispiel wurde durch die Implementierung einer REST-ähnlichen Schnittstelle eine Designschwäche im Datenmodell ermittelt. Dabei war der Anlass für die nähere Betrachtung des Datenmodells die Unfähigkeit, den Genehmigungsschritt sauber als neue Ressource darzustellen. Stattdessen wurde ein funktionale Beschreibungsstil genutzt.

Ich möchte nicht so weit gehen, dass jeder problematischen Formulierung im REST Architekturstil ein Designproblem zugrunde liegt, aber in diesem Fall hätte eine SOAP Schnittstelle unter Nutzung der bestehenden Services definitiv nicht den Blick zurück auf die Gestaltung des Datenmodells erzwungen. Insofern ist eine REST-ähnliche Remote Schnittstelle nicht nur Selbstzweck sondern eben durchaus auch - in geringem Maße - als zusätzliche Überprüfung des verwendeten Modells nutzbar. Ein Seiteneffekt der in diesem Fall zwar nicht explizit angestrebt aber dennoch dankbar genutzt wurde.

Weitere Artikel: