Implementierung einer „State Machine“ in Adobe Flex
Programme im öffentlichen Bereich (z.B. für Paketautomaten) müssen stabil und bedienerfreundlich sein. Ein Programm ist dabei ständig verschiedenen Events ausgesetzt, die sowohl aus den GUI-Controls als auch den Hardware-Elementen (z.B. Schließfächern) herrühren.
Innerhalb der fachlichen Prozesse steuern Events den weiteren Programmablauf, hier am Beispiel eines Prozesses für das Einlegen eines Pakets:
- Der Benutzer scannt einen Barcode auf dem Paket.
- Danach soll der Benutzer eine Fachgröße durch Button-Klick auswählen.
- Das Programm öffnet danach automatisch ein Fach der gewählten Größe.
- Das Programm wartet anschließend bis das geöffnete Fach vom Benutzer geschlossen wurde. (nachdem etwas eingelegt wurde)
- Abschließend muss der Benutzer angeben, ob das Paket eingelegt werden konnte oder eine andere Fachgröße ausgewählt werden soll.
Das folgende Diagramm veranschaulicht den Prozess grafisch:

Solche ereignisgesteuerten Abläufen machen die Komplexität der Anwendung aus. Entscheidend für die Stabilität ist dabei, dass Fehlbedienungen und unerwartet eingehende Events aus der GUI oder der Hardware keine unangenehmen Überraschungen im Programmverhalten hervorrufen.
Die Abbildung zeigt eine beispielhafte GUI und welchen Ereignissen das Programm dabei ausgesetzt ist. Letztlich sind die Events, die von GUI und Hardware gesendet werden, nicht kontrollierbar.

Kontrollierbar hingegen muss die Programmreaktion auf diese Events sein:
- Der erste Klick eines der Auswahlbuttons öffnet ein entsprechendes Fach dieser Größe. Mehrfaches Klicken darf nicht mehrere Fächer dieser Größe öffnen!
- Quasi-gleichzeitiges Klicken mehrerer Auswahlbuttons darf nicht mehrere Fächer unterschiedlicher Größe öffnen!
- Klick auf den „Back“ Button bricht den Prozess sofort ab. Wird quasi-gleichzeitig auch ein Auswahlbutton gedrückt, darf jedoch kein Fach geöffnet werden!
- Das Schließen eines offenstehenden Fachs darf nur dann den Programmablauf beeinflussen, wenn das Programm auf das Schließen von Fächern wartet, jedoch nicht während der Auswahl der Fachgröße.
Um solche Situation abfangen zu können, werden Prozesse als State-Transition-Diagramme dargestellt und die Programmreaktionen nicht direkt an die von extern eingehenden Events, sondern an definierte Zustandsübergänge (Transitions) gekoppelt. Events, für die im aktuellen Programmzustand keine Transition definiert ist, können ignoriert werden.
Am Beispiel einer mit Adobe Flex geschriebenen Anwendung stellen wir eine Implementierung der StateMachine vor. Die States und Transitions können in MXML deklariert werden. So ergibt sich eine logische Einheit einer Klasse und der zugehörigen Konfiguration.
DropParcelController.mxml
<?xml version="1.0"?>
<fx:Object xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:sm="de.viaboxx.statemachine.*">
<fx:Script><![CDATA[
public function sizeSelected(boxEvent:BoxEvent):void {
stateMachine.actionForEvent(boxEvent)
}
public function sizeSelectedTransition(boxEvent:BoxEvent):void {
boxSize = boxEvent.boxSize;
sizeSelectedTimestamp = new Date();
}
public function boxClosed(boxNumber:int):void {
if (boxNumber == this.boxNumber) {
stateMachine.actionForEventType(BoxEvent.BOX_CLOSED);
}
}
public function clear():void {
parcelIdentification = '';
barcodeErrorMessage = '';
boxSize = '';
}
[Mediate(event="ProductEvent.IDENTCODE_VERIFIED")]
[Mediate(event="ProductEvent.IDENTCODE_NOT_VERIFIED")]
[Mediate(event="ParcelEvent.PARCEL_DROPPED")]
[Mediate(event="UserEvent.LOGOUT")]
[Mediate(event="UserEvent.TIMEOUT")]
[Mediate(event='BoxEvent.AVAILABLE_BOXES')]
public function handleStateTransition(event:Event):void {
stateMachine.actionForEvent(event)
}
]]></fx:Script>
<fx:Declarations>
<sm:StateMachine id="stateMachine" initial="{scanBarcode}">
<!-- global transitions -->
<sm:transitions>
<sm:Transition forEvent="LOGOUT" to="{scanBarcode}" action="clear"/>
<sm:Transition forEvent="TIMEOUT" to="{scanBarcode}" action="clear"/>
</sm:transitions>
<sm:declarations>
<sm:State id="scanBarcode">
<sm:transitions>
<sm:Transition forEvent="AVAILABLE_BOXES" to="{chooseBoxSize}"/>
</sm:transitions>
</sm:State>
<sm:State id="chooseBoxSize">
<sm:transitions>
<sm:Transition forEvent="BACK" to="{scanBarcode}"/>
<sm:Transition forEvent="SIZE_SELECTED" to="{boxOpen}"
action="sizeSelectedTransition"/>
</sm:transitions>
</sm:State>
<sm:State id="boxOpen">
<sm:transitions>
<sm:Transition forEvent="OPEN_BOX_FAILED" to="{chooseBoxSize}"/>
<sm:Transition forEvent="BOX_CLOSED" to="{dropParcelQuestion}"/>
</sm:transitions>
</sm:State>
<sm:State id="dropParcelQuestion">
<sm:transitions>
<sm:Transition forEvent="ANSWER_NO" to="{chooseBoxSize}"/>
<sm:Transition forEvent="ANSWER_YES" to="{scanBarcode}" action="parcelDropped"/>
</sm:transitions>
</sm:State>
</sm:declarations>
</sm:StateMachine>
</fx:Declarations>
</fx:Object>
Der Sourcecode der Klassen StateMachine, State und Transition ist weitgehend trivial und als Download verfügbar. Sofern eine Methode (mit oder ohne dem Event als Parameter) beim Zustandsübergang aufgerufen werden soll, wird der Funktionsname (Function pointer) als action bei der Transition konfiguriert.
Alle Events, auf die z.B. mit dem Mediate-Tag des Swiz Frameworks gelauscht werden kann und für die Zustandsübergänge definiert sind, können über die Methode
stateMachine.actionForEvent(event)
behandelt werden. Diese führt bei einem Zustandsübergang auch die action der jeweiligen Transition aus.
In der MXML-View können die Zustände direkt an den current-State der State-Machine gebunden werden:
currentState="{returnParcelPM.stateMachine.current.id}"
So wird bei einem Zustandsübergang die entsprechende View sichtbar.
DropParcelFlow.mxml
<s:Group
xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:shared_delivery="de.viaboxx.flexClient.views.shared.delivery.*"
xmlns:returnParcel="de.viaboxx.flexClient.views.returnParcel.*"
width="100%" height="100%"
currentState="{returnParcelPM.stateMachine.current.id}"
>
<s:states>
<s:State name="barcode"/>
<s:State name="boxSizeChooser"/>
<s:State name="boxOpen"/>
<s:State name="dropParcelQuestion"/>
</s:states>
...
<delivery:Barcode id="barcode" includeIn="barcode"
identification="{returnParcelPM.parcelIdentification}"
errorMessage="{returnParcelPM.barcodeErrorMessage}"
isScanning="@{returnParcelPM.isScanning}"
parcel_event_barcode_entered="returnParcelPM.barcodeEntered(event)"
cancel="returnParcelPM.cancel()"/>
<delivery:BoxSizeChooser includeIn="boxSizeChooser"
box_event_size_selected="returnParcelPM.sizeSelected(event)"
back="returnParcelPM.backFromBoxSizeChooser()"/>
<delivery:BoxOpenDeliver includeIn="boxOpen"/>
<delivery:DropParcelQuestion includeIn="dropParcelQuestion"
boxTooSmall="returnParcelPM.boxTooSmall()"
parcelDropped="returnParcelPM.dropParcel()"/>
</s:Group>
Durch die deklarative Implementierung der State-Transitions ergeben sich weitere Vorteile:
- Fehleingaben müssen nicht durch „Tricks“ in der GUI vermieden werden
- der gesamte fachliche Prozess spiegelt sich übersichtlich im Programmcode wieder
- die Testbarkeit des Prozesses wird deutlich verbessert
- die Wartbarkeit des Codes (z.B. Einfügen weiterer Prozessschritte, Umstellung der Reihenfolge etc.) wird einfacher.
- Seiteneffekte durch unerwartete Events und bei Software-Änderungen werden reduziert.
Dieses Verfahren und die State-Machine sind flexibel einsetzbar.
Source Code zum Download: statemachine
