Agile Modellierung mit
UML
Loading

5.4 Ausführung von Statecharts

Wie David Harel, dem die Erfindung der Statecharts zugeschrieben wird, gerne bemerkt, „Buildings are there to be, but Software is there to do“. Da Statecharts in natürlicher Weise zur vollständigen Beschreibung von Verhalten genutzt werden können, ist der Aspekt der Codegenerierung, also der Ausführung von Statecharts, von besonderem Interesse. Deshalb wird in diesem Abschnitt auf Realisierungsstrategien für Statecharts eingegangen, ohne jedoch vollständige Übersetzungsalgorithmen zu beschreiben. Das Ziel dieses Abschnitts ist es auch, dem Leser durch die Beschreibung der Beziehung zwischen syntaktischen Konzepten der Statecharts und Java-Codeelementen zum einen die Möglichkeit zu geben, manuell ein Statechart in Code zu übersetzen. Genauso wichtig ist die dadurch entstehende Hilfestellung zum Verständnis von Statecharts und ihrer Verwendung bei der Codegenerierung. Die Übersetzung der Statecharts in Java dient sowohl als Semantikdefinition, als auch zur Verbesserung des intuitiven Zugangs den Statecharts. Die nachfolgend diskutierten Realisierungsstrategien zeigen alternative Umsetzungsmöglichkeiten von Statecharts in Java, die jedoch semantisch äquivalent sind. Die Auswahl der für ein Projekt geeigneten Umsetzungsstrategie hängt daher von der gewünschten Flexibilität und Effizienzüberlegungen ab.

Die Codegenerierung aus Statecharts ist noch keineswegs so verbreitet wie die auf Basis von Klassendiagrammen, jedoch nimmt die Verwendung von Statecharts für die Implementierung stetig zu. Die eingebetteten Systeme sind hier der Wegbereiter: „... automatically generated code can and is being used today in a variety of hard real-time and embedded systems.“ [Dou99, S. 156].

Die nachfolgend diskutierten Varianten zur Umsetzung sind keineswegs vollständig, sondern stellen nur einen Ausschnitt der Umsetzungsmöglichkeiten dar. Die bereits eingeführten Stereotypen statedefining, completion:ignore und weitere können zur Auswahl zwischen diesen Varianten verwendet werden. Nicht alles lässt sich jedoch bereits mit Stereotypen beschreiben, weshalb die Verwendung von Templates und Skripten für die Codegenerierung von Statecharts sehr hilfreich ist.

5.4.1 Methoden-Statecharts

Die Zustände eines Statecharts können auf verschiedene Arten interpretiert werden. In einem Methoden-Statechart, wie dem in Abbildung 5.25, repräsentieren die Diagrammzustände Zwischenzustände innerhalb des Ablaufs einer Methode. Die Diagrammzustände werden deshalb durch den Programmzähler unterschieden. Diese Diagrammzustände können noch einmal in zwei Klassen unterschieden werden. Zum einen gibt es Diagrammzustände, die Stellen zwischen Anweisungen entsprechen und deren Fortführung durch spontane ε-Transitionen erfolgt. Zum anderen gibt es Zustände, in denen auf das Ergebnis eines Methodenaufrufs gewartet wird, der in einer ankommenden Transition gestartet wurde. Während die Quellzustände spontaner Transitionen vor allem dazu genutzt werden, den Kontrollfluss, wie zum Beispiel Verzweigungen, zu modellieren, stellen Quellzustände von Transitionen, die mit dem Stimulus return markiert sind, echte Unterbrechungen der Ausführungen einer Methode dar.

Die Umsetzung eines Methoden-Statecharts erfolgt kanonisch durch die Generierung der beschriebenen Methode und ihres Rumpfs.


Abbildung 5.25: Repräsentation von Zwischenzustände einer Methode

Von besonderem Interesse ist die Behandlung von ε-Schleifen innerhalb eines Statecharts. Die Existenz einer ε-Schleife bedeutet, dass der Kontrollfluss innerhalb eines Methoden-Statecharts eine Schleife besitzt. Das Statechart selbst ist daher nicht mehr vollständig in der Lage, das Verhalten der Methode zu beschreiben. Die Initialisierung, der Rumpf und die Abbruchbedingung der Schleife sind jeweils durch Aktionen zu beschreiben. Dabei ist nicht gesichert, dass die Schleife definitiv abbricht. Dennoch kann davon ausgegangen werden, dass modelliertes Verhalten grundsätzlich terminiert. Diese Annahme ist aus pragmatischen Gründen sinnvoll, weil in der vorgeschlagenen Entwicklungsmethode Schleifen grundsätzlich mit Tests so überprüft werden, dass eine nichtterminierende Schleife entdeckt werden würde.

5.4.2 Umsetzung der Zustände

Die Statecharts, die nicht zur Darstellung eines Methodenablaufs dienen, beinhalten keine Kontrollzustände und daher auch keine spontanen Transitionen. Die Zustände eines solchen Statechart entsprechen daher Datenzuständen des Objekts. Das heißt, der Diagrammzustand des Statecharts muss aus dem Datenzustand des Objekts rekonstruierbar sein. Dies wurde bereits bei der Transformation in vereinfachte Statecharts in Abschnitt 5.6.3, Band 1 diskutiert und ein Verfahren zur Darstellung von Diagrammzuständen gezeigt. Hier werden weitere Verfahren besprochen.

Die Statecharts, die nicht zur Darstellung eines Methodenablaufs dienen, beinhalten keine Kontrollzustände und daher auch keine spontanen Transitionen. Die Zustände eines solchen Statechart entsprechen daher Datenzuständen des Objekts. Das heißt, der Diagrammzustand des Statecharts muss aus dem Datenzustand des Objekts rekonstruierbar sein. Dies wurde bereits bei der Transformation in vereinfachte Statecharts in Abschnitt 5.6.3, Band 1 diskutiert und ein Verfahren zur Darstellung von Diagrammzuständen gezeigt. Hier werden weitere Verfahren besprochen.

Leichtgewicht: Nutzung der Zustandsinvarianten

Als besonders einfach anzusehen ist die Strategie, die paarweise disjunkten Zustandsinvarianten des vereinfachten Statecharts zu nehmen, um aus einem Objekt den Diagrammzustand jeweils zu berechnen. Abbildung 5.26 zeigt schematisch eine solche Umsetzung. Bei dieser Transformation wird der linken Transition der Vorrang vor der mittleren Transition gegeben, da ihre Vorbedingung zuerst evaluiert wird. Überlappen die beiden Vorbedingungen, also vorb1&&vorb2 ist nicht äquivalent zu false, dann wurde damit eine Entwurfsentscheidung getroffen, die eine spezielle Implementierung aus der Menge der möglichen Implementierungen auswählt.


Abbildung 5.26: Übersetzung unter Ausnutzung von Zustandsinvarianten

Der Vorteil dieser Umsetzung ist, dass kein zusätzliches Attribut notwendig ist, um den Diagrammzustand im Objekt zu speichern. Umgekehrt müssen im ungünstigsten Fall jedoch die Zustandsinvarianten aller Zustände evaluiert werden, wodurch ein deutlicher Effizienzverlust auftreten kann. Deshalb ist diese Umsetzung nur für effizient evaluierbare Zustandsinvarianten sinnvoll. Meist bestehen eine Reihe von Optimierungsmöglichkeiten, weil Zustandsinvarianten „verwandter“ Zustände oft gemeinsame Teilbedingungen besitzen, deren Evaluierung nur einmal notwendig ist. Ähnliches gilt für die Evaluierung der Vorbedingungen von Transitionen mit demselben Quellzustand. Gilt zum Beispiel vorb1<=>!vorb2, so lässt sich die innere if-Abfrage in Abbildung 5.26 deutlich vereinfachen.

Optimierungen bei der Codegenerierung, wie die oben besprochenen, sind im Allgemeinen nicht automatisiert erkennbar. Jedoch besteht die berechtigte Hoffnung, dass die Werkzeuge zur Generierung von Code aus Modellen in der nächsten Zukunft ähnliche Optimierungsalgorithmen einbauen werden, wie dies für Compiler textueller Programmiersprachen heute bereits der Fall ist. Bis dahin ist damit zu rechnen, dass die gesteigerte Entwicklereffektivität bei der Verwendung abstrakter Modelle zu gewissen Effizienznachteilen beim realisierten Code führt. Unter Umständen ist dann, wie etwa bei den Herstellern eingebetteter Systeme, durchaus zu beobachten, dass nach Fertigstellung der Modelle zu Simulations- und Validierungszwecken ein zusätzlicher manueller Schritt zur Optimierung des Ergebnisses sinnvoll ist. Dafür lassen sich Refactoring-Techniken auf Ebene der Zielsprache ebenso verwenden, wie die Verwendung zusätzlicher Steuerungsmechanismen bei der Generierung des Codes. Beispielsweise können durch die Vergabe von Prioritäten und die Zusicherung der Disjunktheit von Vorbedingungen dem Codegenerator Optimierungen vorgeschlagen werden.

Zustände als Prädikate

Eine Variation der gezeigten Umsetzung ist in 5.27 zu sehen, in der die Auswertung einzelner Zustandsinvarianten und Aktionen in eigene Methoden ausgelagert wurde. Die Hilfsmethoden zur Umsetzung von Aktionen lassen sich dann gegebenenfalls wiederverwenden, wenn verschiedene Transitionen dieselbe Aktion besitzen. Die Auslagerung der Evaluierung von Zustandsinvarianten in eigene Prädikate, die den Zustandsnamen als Prädikatnamen tragen, hat darüber hinaus den Vorteil, dass auf diese Weise der Diagrammzustand des Statecharts an beliebigen Stellen im Code festgestellt werden kann. Dies ist besonders für Methoden hilfreich, deren Verhalten nicht im Statechart spezifiziert ist, aber dennoch von dem im Statechart modellierten Zustandskonzept abhängt.


Abbildung 5.27: Prädikate evaluieren Zustandsinvarianten

Aufzählungsattribut als Speicher für den Zustand

Ist die Evaluierung der Zustandsinvarianten zu ineffizient, so eignet sich der heute meistens genutzte Standardweg, den Diagrammzustand in Form eines Attributs, das eine Aufzählung beinhaltet, explizit im Zustandsraum des Objekts abzulegen. Zur Feststellung des Diagrammzustands reicht es nun, das status-Attribut zu prüfen. Abbildung 5.28 zeigt die verwendbare Codegenerierung.


Abbildung 5.28: Diagrammzustand wird in Attribut gespeichert

Vollständiges Statechart

Die Verwendung eines Attributs zur Speicherung des Diagrammzustands führt zu einer besseren Laufzeiteffizienz, verhindert jedoch nicht, dass weiterhin die Vorbedingungen von alternativen Transitionen geprüft werden müssen. Allerdings kann die jeweils letzte Vorbedingung sowie auch die Zustandsinvariante in einer assert-Anweisung eingesetzt werden, um statt im konstruktiven Anteil der Implementierung zu Testzwecken verwendet zu werden.14


Abbildung 5.29: Verwendung von Zustandsinvarianten als Zusicherungen

Wurde das Statechart nicht, wie in Abschnitt 5.6.3, Band 1 beschrieben, explizit vervollständigt, so kann in Abhängigkeit der gewählten Semantik diese Vervollständigung auch während der Codegenerierung durchgeführt werden. Dazu dient zum Beispiel die default-Anweisung, die alle nicht durch explizite Transitionen abgedeckten Situationen abfangen kann. Auch kann zum Beispiel ein weiterer Wert in der Aufzählung der Zustände in der Form ERROR==-1 eingeführt werden, der in solchen Situationen angenommen wird.

Für das Abfangen von Exceptions aufgrund des Stereotyps exception kann eine umfassende try-catch-Anweisung verwendet werden. Dabei müssen allerdings je nach gewählter Umsetzungsstrategie mehrere Anweisungen in verschiedenen Methoden oder case-Alternativen eingesetzt werden.

Schwergewicht: State-Entwurfsmuster

Eine weitere Möglichkeit zur Umsetzung des Zustandskonzepts ist die Verwendung des State-Entwurfsmusters, zum Beispiel beschrieben in [GHJV94]. Dieses Entwurfsmuster wird als Schwergewicht bezeichnet, da es zu einer Reihe von zusätzlichen Klassen führt. Der Zustand des eigentlich modellierten Objekts wird ausgelagert in eine eigene Zustandsklasse. Diese besitzt mehrere Unterklassen, die je einem Zustand des eigentlich modellierten Objekts entsprechen. Der aktuelle Zustand des Objekts wird durch Referenz auf eines dieser Zustandsobjekte gespeichert. Das Verhalten wird nicht direkt in der modellierten Methode realisiert, sondern an dieses Zustandsobjekt delegiert. Dadurch wird das im Statechart auf den Transitionen verteilte Verhalten nicht innerhalb einer Methode zusammengefasst, sondern entsprechend der Zustände gruppiert. Dies bietet zum Beispiel die Flexibilität, das Verhalten an einem Quellzustand durch Unterklassenbildung zu modifizieren. Der notationelle und operative Aufwand für das State-Entwurfsmuster ist allerdings immens. So muss ein Management der Zustandsobjekte realisiert werden, das entweder dynamisch solche Objekte erzeugt oder in einem Pool von Objekten speichert. Abbildung 5.30 zeigt daher nur einen kleinen Ausschnitt der Umsetzung in einem State-Entwurfsmuster. Die Codeteile invarianteA’ und aktion1’ sind dabei entsprechend anzupassen, da die darin enthaltenen Attribute und Methodenaufrufe auf das Ursprungsobjekt k statt self zugreifen müssen.


Abbildung 5.30: Schwergewicht: State-Entwurfsmuster

Die Verwendung des State-Entwurfsmusters ist nur zu empfehlen, wenn die dadurch entstehende Flexibilität den Zusatzaufwand bei der Generierung rechtfertigen kann. Da die Umsetzung eines Statecharts in Java-Code normalerweise automatisiert erfolgt und ein manueller Eingriff in den generierten Code im Allgemeinen nicht sinnvoll ist, sollte dementsprechend auch das State-Entwurfsmuster nur selten adäquat sein.

5.4.3 Umsetzung der Transitionen

Stimuli

Wie Abbildung 3.37 zeigt, werden drei Arten von Stimuli unterschieden. Spontane Transitionen und Empfang von return-Ergebnissen treten nur innerhalb von Methoden-Statecharts auf. Die Übertragung asynchroner Nachrichten und der Methodenaufruf werden im Statechart nicht unterschieden. Erst bei der Umsetzung in Code ist die Unterscheidung zwischen einem Methodenaufruf und der Verwendung von Nachrichtenobjekten zur asynchronen Übertragung relevant. Handelt es sich um einen Methodenaufruf, so wird dieser, wie bereits im letzten Abschnitt beschrieben, durch eine entsprechende Methode implementiert.

Wird eine Objektifizierung der Stimuli vorgenommen, so werden Events typischerweise in Form einer Klassenhierarchie mit einer abstrakten Oberklasse Event umgesetzt, deren Unterklassen jeweils einzelnen Nachrichtentypen entsprechen. Nachrichten werden durch Aufruf des entsprechenden Konstruktors erzeugt und durch einen geeigneten Übertragungs- und Scheduling-Mechanismus beim Zielobjekt zur Ausführung gebracht. Für den Transport dieser Nachrichten stehen eine Reihe von Frameworks und Middleware-Komponenten, wie zum Beispiel Corba [OH98], zur Verfügung. Weitere Varianten sind die Serialisierung der Nachrichtenobjekte zum Beispiel mit XML [W3C00] oder die Übertragung durch ein selbstdefiniertes, typischerweise effizienteres Protokoll. Aufgrund der hohen Auswahl an zur Verfügung stehenden Lösungen soll dieser technische Aspekt der Kommunikation hier nicht weiter vertieft werden. Für die Umsetzung des Statecharts ist letztendlich nur wesentlich, dass das Nachrichtenobjekt an das zu verarbeitende Objekt übergeben wird, indem eine geeignete Methode aufgerufen wird. Abbildung 5.31 skizziert eine mögliche Realisierung auf Basis einer doppelten switch-Anweisung. Durch Definition von Methoden, die jeweils ein spezifisches Nachrichtenobjekt verarbeiten oder durch Auslagerung der Verarbeitung einer Methode auf abhängige Objekte können auch in diesem Fall die bereits früher diskutierten Varianten zur Codegenerierung angewandt werden.


Abbildung 5.31: Verwendung von Event-Objekten als Stimuli

Ein bereits früher diskutierter Vorteil der Verwendung asynchron kommunizierter Nachrichten ist die Vermeidung von rekursiven Objektaufrufen. Die Verarbeitung eines Nachrichtenobjekts findet immer unter exklusivem Zugriff auf den Objektzustand statt. Eine Parallelverarbeitung mehrerer Nachrichten ist ausgeschlossen. Um das sicherzustellen, werden geeignete Synchronisationsmechanismen der Programmiersprache Java eingesetzt. In einem Statechart können auch die Verarbeitung von Nachrichten und Methodenaufrufe gemischt vorkommen. Für das Statechart ist letztendlich unerheblich, ob der Stimulus durch die spezielle Methode receive zur Nachrichtenverarbeitung oder durch einen normalen Methodenaufruf bearbeitet wird. Zu beachten ist nur, dass innerhalb einer Aktion des Statecharts keine weiteren Methodenaufrufe an dasselbe Objekt stattfinden, die bezüglich des Statecharts eine zustandsverändernde Wirkung besitzen. Diese, als Rekursionsfreiheit bezeichnete Bedingung, wurde bereits in den Abschnitt 5.1, Band 1 ausführlich diskutiert.

Aktionen

Die Aktionsbeschreibungen eines Statecharts bestehen aus zwei möglichen Komponenten. Die prozedural formulierten Aktionen können nach Anpassung von Attibutzugriffen, etc. gemäß Abschnitt 5.1 in den generierten Code übernommen werden. Wurde ein zusätzliches Attribut zur Speicherung des Diagrammzustands eingeführt, so ist zum Ende der Aktion eine zusätzliche Zuweisung des neuen Zustands notwendig. Die Umsetzung in Abbildung 5.28 illustriert dies.

Wurden bei den Aktionen zusätzlich oder ausschließlich OCL-Nachbedingungen verwendet, so kann daraus im Allgemeinen kein operationeller Code generiert werden. Der Einsatz eines solchen Statecharts zur Programmierung ist daher nicht möglich. Solche Nachbedingungen werden deshalb typischerweise nur zur abstrakten Spezifikation von Verhalten verwendet, das zum einem bei einer Implementierung manuell in ablauffähigen Code umgesetzt oder zum anderen bei Tests eingesetzt wird. Die Nachbedingungen können deshalb nur mithilfe von ocl-Anweisungen in den Code umgesetzt werden.

Existente Codegenerierungen für Statecharts

Natürlich ist die oben beschriebene Form nicht die erste Form der Umsetzung von Statecharts in Code. Mehrere Werkzeuge, wie zum Beispiel Statemate oder Rhapsody [HN96] sowie einige der neueren UML-basierten Werkzeuge und Ansätze wie [SZ01BS01aBLP01] übersetzen ihre Version der Statecharts in produktiven Code. Dabei werden teilweise auch Konzepte wie parallele Zustände, ein History-Mechanismus, Pseudozustände oder echte Parallelität innerhalb eines Zustands umgesetzt, die hier nicht eingeführt wurden. Dies liegt unter anderem an der bisher dominierenden Verwendung von Statecharts zur Modellierung verteilter und eingebetteter Systeme, die meist einen höheren Kontrollanteil besitzen als die datenlastigen Geschäftssysteme [Dou98Dou99].

Statecharts wurden bereits in [Har87] eingeführt und in [vdB94] wurde die bis dahin entstandene Vielzahl von Semantiken verglichen sowie in [vdB01] die UML-Semantik der Statechart in den Vergleich einbezogen. Ein interessantes Merkmal dieser Semantiken ist, dass sie teilweise unterschiedliches Verhalten beschreiben und damit zu unterschiedlichen Implementierungen der Statecharts führen, teilweise aber auch nur verschiedene Mechanismen nutzen, um den Statecharts die essentiell gleiche Semantik zuzuweisen.

Von besonderer Aufmerksamkeit ist die Behandlung der Priorisierung und Unterbrechbarkeit von Transitionen verschiedener Hierarchieebenen und die damit eng verbundene „Run to Completion“-Problematik für Statecharts. Während für eingebettete Systeme die äußeren Transitionen (bezogen auf den Quellzustand) bevorzugt werden, wird zum Beispiel in [HG97] für objektorientierte Statecharts diese Priorisierung umgekehrt und inneren Transitionen der Vorzug gegeben. Der hier verfolgte Ansatz, dies über Stereotypen dem Modellierer selbst entscheiden zu lassen, führt zu mehr Flexibilität. „Run to Completion“ spielt bei den UML/P-Statecharts keine Rolle, denn wegen dem zugrunde liegenden, auf Java basierendem Maschinenmodell wird davon ausgegangen, dass die Exceptions, die als Stimuli auftreten, durch das Statechart selbst oder eine aus einer Aktion des Statecharts heraus aufgerufenen Methode verursacht sind und daher eine Weiterführung der Transition zu einem natürlichen Ende nur beschränkt sinnvoll ist. Die Möglichkeit paralleler Verarbeitung von Transitionen in demselben Objekt wird durch die grundsätzlich verwendete Synchronisation auf zustandsbehafteten Objekten ausgeschlossen.


Bernhard Rumpe. Agile Modellierung mit UML. Springer 2012