Im ersten und zweiten Teil dieser Reihe hatten wir Zustandsautomaten, grundlegende Elemente der grafischen Modellierungssprache und zeitgesteuerte Zustandsübergänge kennengelernt. Wie sich diese Konzepte mit dem Modellierungswerkzeug YAKINDU Statechart Tools umsetzen lassen, veranschaulichte das Beispiel einer Jalousiesteuerung.
Im dritten Teil der Serie gehen wir der Frage nach, wie aus modellierten Zustandsautomaten Programmcode wird. Wer eine Jalousiesteuerung modelliert und sie nicht nur als Beispiel braucht, will sie ja auf einem wirklichen Steuergerät zum Einsatz bringen und braucht dazu ausführbaren Programmcode. Wir werden verschiedene Implementierungsansätze für Zustandsautomaten kennenlernen und in diesem Teil mit der Switch-Anweisung beginnen.
Die Realisierung eines Zustandsautomaten in Software mit Hilfe einer Switch-Anweisung ist der wohl gebräuchlichste Ansatz. Oftmals implementieren Entwickler mit Switch-Anweisungen Zustandsautomaten, auch wenn sie sich gar nicht dessen bewusst sind, dass es sich um Zustandsautomaten handelt.
Jeder Zustand des Automaten entspricht einer der Case-Klauseln der Switch-Anweisung. Das Programm springt in diejenige Case-Klausel, die dem aktiven Zustand entspricht. Innerhalb dieser Case-Klausel führt es zustandsspezifische Anweisungen aus, prüft, ob beziehungsweise welche Events vorliegen und aktiviert in Abhängigkeit davon gegebenenfalls einen anderen Zustand.
Grundsätzlich sind dazu zwei Varianten üblich:
Auch eine Kombination beider Ansätze ist denkbar. Das Ausführen der Switch-Anweisung und das Bearbeiten der vorliegenden Events heißt Run-to-completion-Schritt (RTC).
Die Grundstruktur der Implementierung lässt sich am Beispiel einer Verkehrsampel sehr einfach skizzieren, weil jedem Zustand der Ampel stets genau ein Folgezustand zugeordnet ist: Auf Rot folgt Rot-Gelb, dann Grün, dann Gelb und danach wieder Rot. Dieser Automat kommt – zunächst jedenfalls – ohne Events aus, was sich in einer erfreulich übersichtlichen Programmierung niederschlägt.
Als Implementierungssprache wählen wir hier Java, aber selbstverständlich lässt sich dieses Prinzip analog auch in anderen Programmiersprachen umsetzen. Zunächst definieren wir eine Aufzählung mit sämtlichen Zuständen des Zustandsautomaten, sprich: mit den Ampelphasen.
enum State { RED, RED_YELLOW, GREEN, YELLOW }
Die Variable activeState enthält den jeweils aktuellen Zustand; initial ist das Rot.
State activeState = State.RED;
Die Methode stateMachine implementiert den eigentlichen Zustandsautomaten. Ihr Herzstück ist eine Switch-Anweisung, die in Abhängigkeit vom aktuellen Zustand durch Zuweisung an activeState den jeweils nächsten Zustand aktiviert. Dies geschieht in diesem Beispiel zyklisch in einer Endlosschleife.
public void stateMachine() { while (true) { switch (activeState) { case RED: { activeState = State.RED_YELLOW; break; } case RED_YELLOW: { activeState = State.GREEN; break; } case GREEN: { activeState = State.YELLOW; break; } case YELLOW: { activeState = State.RED; break; } } } }
Das nächste Beispiel geht einen Schritt weiter und berücksichtigt Events. Dazu greifen wir auf die aus Teil 1 bekannte Jalousiesteuerung zurück und zwar auf die in der folgenden Abbildung gezeigte Ausführung.
Die Implementierung in Java fasst die Zustände des Automaten im Enum State zusammen:
enum State { INITIAL, IDLE, MOVING_UP, MOVING_DOWN }
Auch die Events lassen sich in einem Enum darstellen:
enum Event { USER_UP, USER_DOWN, POSSENSOR_UPPER_POSITION, POSSENSOR_LOWER_POSITION }
Die Variable activeState wird mit dem Initialzustand des Automaten vorbelegt:
State activeState = State.INITIAL;
Die Collection<Event> events nimmt die eintreffenden Events auf:
Collection<Event> events = new ArrayList<Event>();
Entwicklern, die mit Java nicht vertraut sind, sollten an dieser Stelle wissen, dass Collection<Event> einen Datentyp bezeichnet, der eine Gruppe von Event-Objekten verwaltet. Das Beispiel verwendet als konkrete Implementierung eine ArrayList<Event>, eine Liste von Event-Objekten, die intern mit Hilfe eines Arrays realisiert ist.
Dahinter steckt die folgende Idee: Wie wir gesehen haben, führt der Zustandsautomat die Switch-Anweisung ständig wiederholt in einer Endlosschleife aus. Grundsätzlich sollte der Zustandsautomat aber nur dann etwas tun, wenn es tatsächlich etwas zu tun gibt, etwa um Strom zu sparen oder auf einem Multitaskingsystem nicht unnötig CPU-Kapazität zu blockieren. Daher läuft die Endlosschleife nicht in der höchstmöglichen Geschwindigkeit, sondern in einem geeigneten, niedrigeren Takt. Das heißt, der Automat führt einen Zyklus aus (Run-to-completion-Schritt) und pausiert dann eine Zeit lang. Der Stromspareffekt ist besonders für eingebettete Geräte ohne externe Stromversorgung wichtig. Im Beispiel beträgt die Wartezeit zwischen zwei Zyklen 100 Millisekunden:
long clockPulse = 100;
Innerhalb dieser Wartezeit können Events eintreffen. Wie dies geschieht, lassen wir an dieser Stelle außen vor. Jedenfalls gelangen diese Events in die oben definierte Collection und lassen sich im nächsten Verarbeitungszyklus auswerten. Befindet sich die Jalousiesteuerung etwa im Zustand Idle und liegt das Ereignis User.up vor, so wechselt sie in den Zustand Moving up. Beim Event User.down erfolgt ein Zustandsübergang zu Moving down. Der Java-Code, der das erledigt, sieht so aus:
case IDLE: { if (events.contains(Event.USER_UP)) activeState = State.MOVING_UP; else if (events.contains(Event.USER_DOWN)) activeState = State.MOVING_DOWN; break; }
Sollten irgendwelche anderen Events eintreffen, während Idle der aktive Zustand ist, fallen sie unter den Tisch. Ein Zustandsautomat reagiert per Definition nur auf diejenigen Events, für die der jeweilige aktive Zustand "sensibel" ist.
Übrigens können auch mehrere Ereignisse gleichzeitig vorliegen. Je geringer der Takt ist, desto höher ist die Wahrscheinlichkeit, dass ein Benutzer zwischen zwei Verarbeitungszyklen sowohl die [↑]- wie auch die [↓]-Taste drückt. In der obigen Implementierung "gewinnt" in diesem Fall immer die [↑]-Taste. Wer der [↓]-Taste Vorrang geben will, muss die Implementierung entsprechend anpassen.
Hier die vollständige Methode stateMachine, die das Verhalten des Zustandsautomaten implementiert:
public void stateMachine() { while (true) { /* Act upon the active state: */ switch (activeState) { case INITIAL: { activeState = State.IDLE; break; } case IDLE: { if (events.contains(Event.USER_UP)) activeState = State.MOVING_UP; else if (events.contains(Event.USER_DOWN)) activeState = State.MOVING_DOWN; break; } case MOVING_UP: { if (events.contains(Event.POSSENSOR_UPPER_POSITION) || events.contains(Event.USER_DOWN)) activeState = State.IDLE; break; } case MOVING_DOWN: { if (events.contains(Event.POSSENSOR_LOWER_POSITION) || events.contains(Event.USER_UP)) activeState = State.IDLE; break; } default: { /* Should never happen. */ throw (new IllegalStateException()); } }
Wir haben bereits gesehen: Innerhalb eines Taktes führt höchstens ein Event zu einem Zustandsübergang. Das ist an dieser Stelle bereits geschehen, sofern ein passendes Event vorlag. Egal, ob wir uns nun in einem neuen Zustand oder noch im selben wie zuvor befinden, die im letzten Takt eingetroffenen Events besitzen keine Relevanz mehr. Für den nächsten Takt müssen wir sie aus dem Weg räumen:
/* Clear events: */ events.clear();
Damit ist dieser Verarbeitungszyklus abgeschlossen. Der Zustandsautomat pausiert nun clockPulse Millisekunden:
/* Pause until the next run to completion step is due: */ try { Thread.sleep(clockPulse); } catch (InterruptedException e) { // Ignore } } }
In den folgenden Teilen dieser Reihe werden wir zwei weitere Implementierungsansätze kennenlernen: die Darstellung von Zustandsautomaten in einer Tabelle und – als objektorientierte Variante – das Software-Entwurfsmuster State Pattern.
Darüber hinaus werden wir einen kurzen Ausblick auf die automatische Codegenerierung von YAKINDU Statechart Tools riskieren.
Alle Teile dieser Blogserie haben wir übrigens auch in einem Whitepaper zusammengefasst, dass ihr euch kostenfrei herunterladen könnt.