Anleitung StateTransitionEngine
Die StateTransitionEngine (Zustands-Übergangs-Maschine) stellt eine Lösung zur Realisierung von
Workflows (Fluss-Steuerungen) oder von zustandsbehafteten Systemen dar. Entsprechende grafische
Modelle sind Aktivitätsdiagramme oder Petri-Netze.
+-----------+ +-----------+
+->| Zustand 1 |----------->| Zustand 2 |--+
| +-----------+ +-----------+ |
| |
| +-----------+ +-----------+ |
+--| Zustand 4 |<-----------| Zustand 3 |<-+
+-----------+ +-----------+
Die Struktur eines Zustandsübergangsdiagrammes wird in eine Tabellendarstellung transformiert.
Die Steuertabelle hat folgenden Aufbau:
aktueller Status SourceState | Ereignis StateTransitEvent | Pos | User-Rolle isUserPermitted() | Bedingung beforeStateChanged() | Aktion vor Wechsel beforeStateChanged() | Ziel-Status DestinationState | Aktion nach Wechsel afterStateChanged() |
Start | next | 0 | User | | | Lauf | i = 0 |
Lauf | next | 0 | User | i < 3 | | Lauf | i++ |
0 | User | | | Stop | |
Stop | | | | | | | |
Obiges Beispiel zeigt drei mögliche Zustände, Start, Lauf und Stop, wobei der
Zustand Lauf dreimal wiederholt erreicht wird.
Das heisst also, dass zu jedem Status eine Tabelle mit möglichen Ereignissen und
zu jedem Ereignis eine Tabelle mit Bedingungen, entsprechenden Ziel-Zuständen
und Aktionen (Methoden-Aufrufe) vor und nach dem Wechsel exisitiert. Jeder
Zielzustand mit den dazugehörigen Bedingungen hat eine bestimmte Position in der
Reihenfolge der Zielzustände je Ereignis, um die Reihenfolge beim Bestimmen des
passenden Zielzustandes eindeutig festzulegen.
In der Implementierung wird die Status-Tabelle auf einen Baum mit folgender Struktur abgebildet.
StateTransitionEngine
|
+--> SourceState (Map)
|
+--> StateTransitEvent (Map)
|
+--> DestinationState (List) , isUserPermitted() , isTransitConditionTrue()
Eine wichtige Regel beim Entwurf der Zustands-Übergangs-Tabelle ist, dass
StateTransitEvents stets Imperative sein sollten:
Gib in Bearbeitung!
Gib frei!
Setze auf Status "Geprüft"!
Weise zurück!
Lösche!
während Stati immer eine Zustand beschreiben:
Ist in Bearbeitung.
Ist Freigegeben.
Wurde Geprüft.
Ist Zurückgewiesen.
Ist Gelöscht.
Prinzipiell könnte man die StateTransitEvents auch
StateTransitActions nennen. Ich habe mich jetzt erst mal auf den Namen
Event festgelegt. Wahrscheinlich spielt es keine Rolle. Beim Entwurf der
StateTransitionMap sollte das einem aber bewusst sein.
Wenn man für die States (Source und Destiantion) und die Events kryptische
(technische) Namen verwendet, bietet es sich an, vor der Anzeige auf der GUI für
Statusinformation und menüartige Event-Auswahl (Selectbox, Links, Buttons) eine
Internationalisierungs-Logik (i18n) mit RessourceBundles dazwischenzuschalten.
Dadurch erhält man neben der Internationalisierung noch eine Übersetzung von
technischer zu fachlicher Notation.
Das Definieren/Erzeugen der Zustands-Tabelle
Zuerst wird eine StateTransitionEngine erzeugt:
// create engine
StateTransitionEngine stateTransEng = new StateTransitionEngine();
Dieser StateTransitionEngine können beliebig viele Zustände zugeordnet werden.
Da diese Zustände Anfangspunkte von Zustandsübergängen sind, nennen sie sich
SourceState.
SourceState stateStart = new SourceState("Start");
stateTransEng.add(stateStart);
Jedem SourceState können beliebig viele Ereignisse (StateTransitEvent) zum
Zustandsübergang zugeordnet werden. Unterschiedliche Ereignisse können dabei
unterschiedliche Zielzustände spezifizieren.
+-------------+
| SourceState |
+-------------+
| +--------------------+
+--- StateTransitEvent 1 ---->| DestinationState 1 |
| +--------------------+
|
| +--------------------+
+--- StateTransitEvent 2 ---->| DestinationState 2 |
+--------------------+
// create from state start event next
StateTransitEvent nextFromStateStartEvent = new StateTransitEvent("next");
stateStart.add(nextFromStateStartEvent);
Jedem Zustands-Übergangs-Ereignis (StateTransitEvent) kann mindestens ein
Zielzustand zugeordnet werden.
// create destination state start to second
DestinationState secondDestinationState = new DestinationState("Second");
nextFromStateStartEvent.add(secondDestinationState);
Alternativ ist es möglich, jedem Zustands-Übergangs-Ereignis (StateTransitEvent)
beliebig viele Zielzustände zuzuordnen, die sich durch ihre Übergangs-
Bedingungen (Methode boolean isTransitConditionTrue() ) unterscheiden sollten.
Dazu kann eine eigene Klasse von der Klasse DestinationState abgeleitet werden,
um die Methoden isTransitConditionTrue, transitConditionDescription,
beforeStateChanged und afterStateChanged zu überschreiben.
// create destination state start to second
// Die eigene (Anwender-)Klasse SecondDestinationState
// erbt von DestinationState
DestinationState secondDestinationState = new SecondDestinationState();
nextFromStateStartEvent.add(secondDestinationState);
Die mit einenm StateTransitEvent verbundenen DestinationState´s werden in der
Reihenfolge, in welcher sie aufaddiert wurden (Methode add oder Reihenfolge in
der XML-Datei) nach dem Erfüllen der Umschalt-Bedingung
(isTransitConditionTrue()-Methode) abgesucht. Der erste passende
DestinationState wird angesprungen.
Dabei werden die Aktions-Methoden beforeStateChanged vor und afterStateChanged
nach dem Umschalten des Status ausgeführt.
Durch den Einbau der User-Rollen-basierten Bedingungsabfrage werden diese in die
Prüfung, ob der Zielzustand passt, mit einbezogen.
Nach dem kompletten Aufbau der Zustands-Übergangs-Tabelle sollte ihre innere
Struktur geprüft werden. Dabei wird geprüft, ob zu jedem DestinationState ein
passender SourceState existiert. Der Einbau weiterer Prüfungen ist vorgesehen,
zum Beispiel, ob jeder SourceState, ausser initial, über einen DestinationState
erreicht werden kann. Beim Einlesen aus einer XML-Datei ist der check()-Aufruf
nicht unbedingt nötig, er wird automatisch erledigt.
// check validity of state map
if (!stateTransEng.check()) {
System.out.println("state map is invalid");
System.out.println("" + stateTransEng.getCheckMessages());
System.exit(0);
oder
throw new StateTransitionEngineIsInvalidException();
}
else {
System.out.println("state map is valid");
}
Anschliessend wird mindestens ein State-Objekt mit Bezug auf die jetzt fertige
StateTransitionEngine erzeugt.
// create application state
ApplicationState appState = new ApplicationState(stateTransEng) ;
Durch das Abschicken der Events kann der Status weitergeschaltet werden.
appState.perform("next", ctx);//ohne User-Rolle
appState.perform("next", ctx, strUserRole);//mit User-Rolle
Als Kontext kann eine beliebiges Objekt, zum Beispiel auch this übergeben
werden. Dieses Objekt wird in die anwendungsorientierte Prüfung der
Umschaltbedingungen einbezogen.
Hier der entsprechende Code aus SecondSecondDestinationState:
/**
* check condition with overgiven context method to implements by the user of the StateTransitionEngine
*/
public boolean isTransitConditionTrue(Object paObjStateContext) {
Context contxt = (Context) paObjStateContext;
if (contxt == null) {
System.out.println("overgiven context is null");
return true;
}
// perform if Context.i <= 3
return contxt.i < 3;
}// end method
Generieren aus XML-File
Eine XML-Datei kann als InputStream mit der Methode readXmlInputStream aus dem
gleichen Verzeichnis wie die Source-Klassen, bzw. aus einem jar-File eingelesen
werden.
InputStream xmlInpStream = stateTransEng.getClass().getResourceAsStream(
"/de/cnc/statetransitiondemo/DemoStateTransitionMap.xml");
stateTransEng.readXmlInputStream(xmlInpStream);
stateTransEng.check();//bei Einlesen aus XML nicht unbedingt nötig, wird automatisch erledigt
Als XML-Reader dient die Open-Source-Library kXml. Deshalb muss die
mitgelieferte kxml.jar in den CLASSPATH eingebunden werden.
Hier ist die XML-Definitionsdatei für die oben als Beispiel angegebene
Zustands-Übergangs-Tabelle, die auch der aufgebauten Zustands-Übergangs-Tabelle
aus StateDemo.java entspricht.
siehe hierzu die mitgelieferte Beispieldatei /statetransitiondemo/DemoStateTransitionMap.xml
Tags
-StateTransitionMap äusseres Tag
Attribute:
checkUserRole (true/false), optional, schaltet checkUserRole ein/aus
darf folgendes Tag enthalten: SourceState
-SourceState darf nur im Tag StateTransitionMap erscheinen
Attribute:
Name Pflicht-Attribut
initial (true/false), optional, setzt den State als Anfangszustand
darf folgendes Tag enthalten: StateTransitEvent
-StateTransitEvent darf nur im Tag SourceState erscheinen
Attribute:
Name Pflicht-Attribut
darf folgendes Tag enthalten: DestinationState
-DestinationState darf nur im Tag StateTransitEvent erscheinen
Attribute:
Remark optionaler Kommentar
DestinationStateName Pflicht-Attribut, Name des anzusteuernden States
AllPermitted optional, erlaubt diesen DestinationState für alle User-/User-Rollen, wenn true
(Wenn das Attribut AllPermitted auftaucht, schaltet der XML-Parser den checkUserRole für die StateTransitionEngine ein)
UserRole optional, darf beliebig oft wiederholt werden, Angabe einer/mehrerer User-Rollen, für die der anzusteuernde Status erlaubt ist
(Wenn das Attribut UserRole auftaucht, schaltet der XML-Parser den checkUserRole für die StateTransitionEngine ein)
(eventuell mit kommaseparierter Liste arbeiten)
darf folgendes Tag enthalten: keines
Einbringen applikationsspezifischen Codes in die Zustands-Tabelle und
Einbeziehen des Applikations-Kontextes in die Prüfung der Bedingungen
Durch Erben von der Klasse DestinationState kann der Benutzer dieser StateTransitionEngine eigenen
Code für die Wechsel-Bedingung (transit condition) sowie die Vor- und Nach-Wechsel-Aktionen
einbringen. Die Möglichkeit zur Verwendung von User-Code schien mir flexibler als eine Realisierung
mit Primitiv-Bedingungen, zu interpretierenden Ausdrücken oder Reflection-Features.
Der Methode
void perform(String paStrEventName, Object paObjStateContext)
in der Klasse StateTransitionEngine kann der Applikations-Kontext als Object übergeben werden.
In den Methoden
boolean isTransitConditionTrue(Object paObjStateContext)
void beforeStateChanged(Object paObjStateContext)
void afterStateChanged(Object paObjStateContext)
der Klasse DestinationState, beziehungsweise ihren anwenderdefinierten
Ableitungen(Kind-Klassen), kann der als Object übergebene Applikations-Kontext
zurück zur Originalklasse gecastet werden, um die Bedingungen gegen den
Applikations-Kontext prüfen zu können sowie Aktionen (pre/after state changed)
auf Applikations-Kontext ausführen zu können. So geht zwar die Compiler-Type-
Prüfung verloren, aber der Applikations-Kontext steht so unmittelbar zur
Verfügung. Eine bessere Lösung ist mir nicht eingefallen.
Anwendung einer Zustands-Übergangs-Tabelle auf mehrere zustandsbehaftete Objekte
Für reale Applikationen ist eine Lösung mit einem Session-, User-, Dokument- und so weiter -weit
gültigen State, aber mit einer applikationsweit gültigen Zustands-Übergangs-Tabelle sinnvoll. Dafür
gibt es die Klasse ApplicationState.
Um das ständige Neuaufbauen der Zustands-Übergangs-Tabelle zu vermeiden,
empfehle ich, die StateTransitionEngine statisch anzulegen.
/**
* Beispiel für statisches Anlegen einer StateTransitionMap
*/
static final StateTransitionEngine staticStateTransEng = initStateTransitionEngine();
/**
* initialize StateTransitionEngine
*/
private static StateTransitionEngine initStateTransitionEngine() {
StateTransitionEngine retStateTransEng = new StateTransitionEngine();
InputStream xmlInpStream = retStateTransEng.getClass().getResourceAsStream(
"/de/cnc/statetransitiondemo/DemoStateTransitionMap.xml");
retStateTransEng.readXmlInputStream(xmlInpStream);
return retStateTransEng;
}// end method
Für den laufenden Test ist dagegen günstiger, bei jedem Request die Zustands-
Übergangs-Tabelle neu aus der XML-datei zu lesen, um das Neustarten des Web-
oder Application-Servers zu vermeiden.
Persistierung des Zustandes
Im Umfeld von Server- oder Host-Applikationen ist das Weitergeben des Zustandes über mehrere
Requests oder Sessions notwendig.
Dies kann ganz leicht durch das Setzen und Abfragen des als String codierten aktuellen Zustandes mit
den Methoden
void setState(String paStrNewState)
String getCurrentStateName()
erreicht werden. Der String kann in einer HTTPSession oder auf einer Datenbank gespeichert werden.
Rollenbasiertes Ändern des Zustandes
Ein Dokument könnte Zustände wie neu, angelegt, in Bearbeitung, geprüft,
abgewiesen, zurückgezogen, gelöscht, veröffentlicht usw. haben. Dabei darf eine
bestimmte Zustandsänderung nur abhängig von der Rolle des aktuellen Users wie
Autor, Redakteur, verantwortlicher Redakteur angestossen werden. Über den
Applikations-Kontext kann das in der Methode
boolean isUserPermitted(String paStrUserRole)
der Klasse DestinationState realisiert werden.
Die Benutzer-Rollen werden in den Ziel-Zuständen (DestinationState) der
Zustands-Übergangs-Tabelle gespeichert. Dadurch ist kein Anwendercode für die
rollenabhängigen Bedingungen nötig.
Für die gesamte StateTransitionEngine kann die Prüfung der Benutzer-Rollen mit der Methode
setUserCheck(boolean paBOn)
eingeschaltet werden. Im XML-File dient dazu das Attribut checkUserRole.
Wenn im XML-File das Attribut UserRole oder AllPermitted im Tag DestinationState
erscheint, schaltet der XML-Parser den UserCheck ein.
Die zulässigen Rollen können zu den jeweiligen DestinationStates mit
addUserRole(String paStrUserRole)
hinzugefügt werden. Im XML-File dient dafür das Attribut UserRole, das beliebig
oft angegeben werden kann.
Mit der Methode
setAllPermitted(boolean paBAllPerm)
kann die Prüfung für DestinationStates, die alle User ansteuern dürfen, komplett
abgeschaltet werden. Im XML-File gibt es das entsprechende Attribut
AllPermitted.
Falls die StateTransitionEngine in einer Applikation mit Prüfung der User-Rollen
gegen eine Datenbank, LDAP oder anderes Backend erfolgt, muss die Methode
boolean isUserPermitted(String) in der Klasse DestinationState entsprechend
ausgetausch werden.
Abfrage der vom aktuellen Zustand aus erreichbaren Ziel-Zustände
Die hinterlegte Zustands-Übergangs-Tabelle erlaubt es, ausgehend vom aktuellen Zustand alle
erreichbaren Zustände aufzulisten. Dies kann zum Erzeugen von Select-Boxen, Buttons oder Links für
die jeweilige Benutzeroberfläche genutzt werden. Dabei wird der Anwendungs-Kontext und die Benutzer-
Rolle mit einbezogen. Dadurch kann auf der Benutzeroberfläche ohne weitere zu erstellende Logik eine
Auswahl der jeweils möglichen Zielzustände angeboten werden.
Siehe Methoden
ApplicationState.getPossibleStateToChangeNames(Object paObjStateContext, String paStrUserRole)
ApplicationState.getPossibleStateToChangeNames(Object paObjStateContext)
ApplicationState.getPossibleStateToChangeNamesAndEventNames(Object paObjStateContext, String paStrUserRole)
ApplicationState.getPossibleStateToChangeNamesAndEventNames(Object paObjStateContext)
StateTransitionEngine.getAllPossibleStateToChangeNames(ApplicationState paAppState)
Realisierung eines StateProcessors
Welcher Programmierer hätte sich es nicht schon mal gewünscht, einen eigenen
Prozessor entwickeln. Ein Prozessor zur automatischen Abarbeitung der Events
lässt sich durch eine einfache Schleife realisieren. Wichtig ist dafür ein
fester vorgegebener StateTransitEvent-Name, hier zum Beispiel "next".
while ( <Abbruchbedingung, z.B. ! "Stop".equals( appState.getCurrentStateName() )> ) {
<Anwendungscode vorher>
appState.perform("next", <Anwendungskontext> );
<Anwendungscode nachher>
}
Debugging
Es ist möglich, die Zustands-Übergangstabelle mit der
StateTransitionEngine.toString()-Methode auszugeben.
Ausserdem kann man sich die abhängig vom Kontext und User erreichbaren Ziel-
Zustände bzw. alle Ziel-Zustände für den jeweiligen Ausgangs-Zustand auflisten
lassen. Siehe hierzu den entsprechenden Abschnitt.
Mögliche Verbesserungen
Alternativ zu ApplicationState(many state, one map) könnte eine Methode
String perform( currentStateString, EventString, contextObject )
den neuen Status in einem einzigen Aufruf umschalten.
Alle Methoden müssen threadsave ausgelegt sein (ausschliesslich auf lokale
Variablen und return-Werte schreiben, globale Variablen nur lesend).
Weiterhin sollten innerhalb der Engine verwendete Klassen und Methoden
entsprechend dem Prinzip des smallest scope möglichst nicht public sein.
SubStateTransitionMaps: Es wäre denkbar, weitere Zustands-Übergangs-Tabellen in
der Art von Unterprogrammen anzuspringen. Dabei benötigt man einen Zustands-
Stack. An die SubStateTransitionMaps könnten der Applikations-Kontext,
SourceStates, StateTransitEvents, DestinationStates und so weiter als Parameter
übergeben werden, um sie generisch wiederverwenden zu können.
Dynamisches Verändern der StateMap zur Laufzeit? Braucht man so etwas? Dabei
müsste geprüft werden, ob nicht ein noch gesetzter Status gelöscht wird und ob
ein zusätzlicher Ziel-Status valid ist (als Quell-Status eingetragen).
HTML-Dump
Debug-Information mit der XML-Zeile in der sich der aktuelle oder ein bestimmter
SourceState, StateTransitEvent und DestinationState befindet.
Einbau Internationalisierung. Methode zum Anmelden eines RessourceBundles
(registerRessourceBundle). Natürlich auch Abfrage (isRessourceRegistred()).
Methoden zur Rückgabe konvertierter oder realer Namen. Eventabarbeitung von
übergeben konvertierten Namen (Rückkonvertierung und dann perform)
Logging für Zustands-Historie