State Pattern-CLI: Realisierung von Transitionen und Menüstruktur
https://bildung.social/@oerinformatik/110504024656041988
https://oer-informatik.de/state-pattern-cli3
tl/dr; (ca. 20 min Lesezeit): Am Beispiel eines Command-Line-Interfaces wird das State-Pattern beispielhaft in Java implementiert - nachdem die Grundstruktur und die Zustands-ENUMS in den vorigen Teilen erstellt wurden, geht es jetzt um die objektorientierte Realisierung der Transitionen und Menüstruktur. (Zuletzt geändert am 11.06.2023)
- Aufbau des State-Patterns allgemein und grundlegende Struktur
- Realisierung der Zustände als Singletons (per Java-ENUMS)
- Realisierung der Transitionen und deren (dieser Artikel)
- Realisierung von Effekten und Guards von Transitionen
Das bisherige Grundgerüst
Es soll eine Kommandozeilen-Benutzeroberfläche für ein Programm erstellt werden, das mit folgendem Zustandsdiagramm modelliert wurde:

In der bisherigen Implementierung wurde bereits das Grundgerüst des State-Diagramms implementiert und die Zustandsobjekte in einem ENUM
gekapselt:

Zur Orientierung nochmal im Schnelldurchlauf die Aufgaben der bisherigen Klassen/Interfaces:
eine Kontext-Klasse
UIContext
, die das Objekt repräsentiert, dessen Zustand sich ändert,einige Zustandsklassen (beispielhaft
AnmeldenUI
undListUI
), die das Verhalten in bestimmten Zuständen implementieren,die
ENUM
-Klasse (UIStateEnum
), die die einzelnen Zustandsobjekte als Singletons zentral verwaltet,ein gemeinsames Interface
UIStateInterface
der Zustandsklassen, dass die Austauschbarkeit der Zustände ermöglicht undeine abstrakte Klasse
AbstractUIState
, die gemeinsame Implementierungen aller Zustandsklassen umsetzt.
Transitionen erweitern
Bislang erfolgt der Zustandsübergang nur fest implementiert in der Kontext-Klasse - das entspricht natürlich nicht dem Wesen einer Benutzeroberfläche. Daher soll das Projekt jetzt auf objektorientierte Art mit den Zustandsübergängen (Transitionen) erweitert werden. Hierzu sind einige Anpassungen fällig:
Wir brauchen eine Klasse
Transition
, die einen einzelnen Zustandsübergang und dessen Eigenschaften erfasst (Transitionsziel, auslösende Trigger, auszuführende Events, Eintrag im Menü).Eine weitere Klasse (
Transitions
) soll die unterschiedlichen Transitionen eines Zustands aggregieren und mögliche Transitionen des aktiven Zustands als Menü ausgeben.Jede Zustandsklasse soll mit
Transisions
über so ein Aggregat der möglichen Zustandsübergänge verfügen. Es wird also ein Attribut vom TypTransisions
inAbstractUIState
eingefügt.

Im Einzelnen:
Die Klasse Transition
, die die Eigenschaften eines Zustandsübergangs modelliert
Im UML-Zustandsdiagramm bestehen Transitionen aus drei Komponenten:
ein Event (Trigger), der den Zustandsübergang auslöst,
ein Guard (Bedingung), der festlegt, unter welchen Bedingungen ein Zustandsübergang stattfinden darf,
ein Effekt, also eine Aktivität, die beim Zustandsübergang ausgeführt wird.
Natürlich ist eine Transition zudem charakterisiert durch den Ausgangs- und Zielzustand.
Wir vereinfachen zunächst etwas und erstellen unsere Klasse mit drei Attributen (sowie zugehörigem Getter und Konstruktor):

Es sind drei Attribute je Transition vorgesehen:
Der Zielzustand
target
(in Form des Enum-Objekts),ein Trigger in Form des Zeichens (Taste), deren Eingabe den Zustandsübergang einleitet,
ein beschreibender Text für das Menü oder die Hilfe, der den Zustandsübergang benennt/beschreibt.
Um später ein Menü mit allen möglichen Zustandsübergängen generieren zu können, lassen wir uns alle Informationen in einer Zeile ausgeben und implementieren daher eine toString()
-Methode.
Der Quelltext dieser Klasse sieht folgendermaßen aus:
public class Transition{
private UIStateEnum target;
private String trigger;
private String menueItem;
public Transition(UIStateEnum target, String trigger, String menueItem){
this.target = target;
this.trigger=trigger;
this.menueItem=menueItem;
}
public UIStateEnum getTarget() {return target;}
public String getTrigger() {return trigger;}
public String getMenueItem() {return menueItem;}
public String toString(){
return "'"+getTrigger()+"': "+getMenueItem()+" ("+getTarget()+")";
}
}
Zusammenfassung aller Transitionen eines Zustands in der Klasse Transitions
Damit sich die Zustandsklassen nicht selbst um alle zugehörigen Transitionen kümmern müssen, wird diese Funktionalität ausgelagert in die Klasse Transitions
. Diese Klasse
speichert, für welchen Ausgangszustand
source
sie zuständig ist,sammelt alle
Transition
-Objekte dieser Zustandsklasse in einerArrayList
(transitionList
),sucht auf Basis eines Triggers (hier: eine Zeichenkette), ob dieser eine Transisiton auslöst (
findTransition(...)
),führt gefundene Transitionen durch und gibt den neuen Zustand zurück (
transist(...)
).
Die genannten Funktionen lassen sich wie folgt modellieren:
Die Implementierung könne etwa so aussehen:
public class Transitions {
private UIStateEnum source;
public Transitions(UIStateEnum source){
this.source = source;
}
private ArrayList<Transition> transitionList = new ArrayList<>();
public void addTransition(UIStateEnum target, String trigger, String menueItem){
transitionList.add(new Transition(target, trigger, menueItem));
}
}
Die beiden weiteren Methoden dieser Klasse verdienen etwas mehr Aufmerksamkeit:
Gibt es für den vorliegenden Trigger (die eingegebene Zeichenkette) eine zuständige Transition? Das prüft findTransition(...)
:
public Transition findTransition(String actualTrigger){
System.out.println("Prüfe, ob der Trigger <"+actualTrigger+"> eine Transition auslöst:");
for(Transition t : transitionList){
System.out.println(" - Prüfe Transition "+t.getTarget().name()+" mit Trigger <" + t.getTrigger()+">");
if (t.getTrigger().equals(actualTrigger)){
System.out.println(" ==> Transition zu "+t.getTarget().name()+" gefunden! (Weitere Suche wird gestoppt)");
return t;
} }
return null;
}
transist(...)
führt die Transition durch:
Ist überhaupt eine Änderung des Zustands erfolgt, oder bleibt alles beim Alten?
Starte die Aktivität beim Verlassen des alten Zustands (
exit()
).Starte die Aktivität beim Betreten des neuen Zustands (
entry()
).Übergebe den aktuellen Zustand an das aufrufende Modul.
Implementiert ist das folgendermaßen:
public UIStateEnum transist(Transition transition){
if ((transition!= null) && (transition.getTarget() != null)){
source.getUIState().exit();
transition.getTarget().getUIState().entry();
return transition.getTarget();
}else{
System.out.println("Keine Transition gefunden! Bleibe bei "+source.getUIState().getClass().getSimpleName());
return source;
}
}
Anpassungen der abstrakten Klasse AbstractUIState
In den Zustandsklassen sollen die Transitionen über Transitions
aggregiert werden. Dazu ist ein Attribut transitions
nötig, eine Methode, die ein neues Transitionsobjekt anlegt (initializeTransitions()
) sowie eine Methode, die neue Transitionen für das jeweilige Zustandsobjekt zu transitions
hinzufügt (addTransition()
):
private Transitions transitions;
public void initializeTransitions() {
transitions = new Transitions(UIStateEnum.getUIStateEnum(this));
}
public void addTransition(UIStateEnum target, String trigger, String menueItem) {
this.transitions.addTransition(target, trigger, menueItem);
}
Jedes Zustandsobjekt soll zukünftig selbst prüfen, ob ein Trigger (hier vereinfacht: ein Tastendruck bzw. eine übergebene Zeichenkette) einen Zustandswechsel hervorruft. Dazu benötigen wir eine weitere Methode, die die Transitionen sucht und ausführt: handleTriggerEvent()
:
public UIStateEnum handleTriggerEvent(String trigger) {
Transition foundTransition = this.transitions.findTransition(trigger);
return this.transitions.transist(foundTransition);
}
Ergänzungen des Interface
Die Methoden handleTriggerEvent()
und initializeTransitions()
müssen für alle Zustandsobjekte über das Interface aufrufbar sein. In Java bestimmt der Instanztyp, welches Verhalten genutzt wird, es können aber jeweils nur Methoden aufgerufen werden, deren Typ der Referenz diese Methoden deklarieren. Da der Aufruf über eine Interface-Referenz von UIStateInterface
erfolgt, müssen wir die neuen Methoden dort aufnehmen:
public interface UIStateInterface {
...
public UIStateEnum handleTriggerEvent(String trigger);
public void initializeTransitions();
...
}
Die Änderungen an Interface und abstrakter Klasse verändern auch das UML-Klassendiagramm:

UIStateInterface
und die Klasse AbstractUIState
mit den ergänzent Methoden initializeTransitions()
und handleTriggerEvent(...)
Transitionen den Zuständen zuordnen
Jetzt folgt etwas Handarbeit: Wir müssen jeden einzelnen Zustandsübergang (Transition
) bei allen Zustandsklassen einfügen (per addTransition()
). Hierzu wird initializeTransitions()
spezialisiert:
Zunächst wird das Transitionsobjekt erstellt, in dem die Methode der Superklasse (
AbstractUIState
) aufgerufen wird.Danach wird für jede gewünschte Transition einmal per
addTransition()
das Transitionsziel (perENUM
), die Trigger-Zeichenkette und ein Titel übergeben.
Im Fall der Zustandsklasse AnmeldenUI
wäre dies:
public void initializeTransitions(){
super.initializeTransitions();
this.addTransition(UIStateEnum.LIST_UI, "l", "Anmelden");
}
Wenn alle im Zustandsdiagramm modellierten Zustandsklassen erstellt wurden, wird der Umfang bei ListUI
deutlich größer - und die Vorteile einer einfachen Navigation per State-Pattern ersichtlich. Ich habe die betreffenden Transitions hier zunächst auskommentiert, da im Tutorial ja bislang nur ANMELDEN_UI
und LIST_UI
umgesetzt wurden - die anderen Verhaltensklassen müssen natürlich analog implementiert werden.
public void initializeTransitions() {
super.initializeTransitions();
this.addTransition(UIStateEnum.ANMELDEN_UI, "a", "Abmelden");
/*
// Für diese Zustandübergange sind die weiteren im Zustands-Diagramm modellierten
// Klassen erforderlich - bitte nur entkommentieren, wenn diese bestehen!
this.addTransition(UIStateEnum.CREATE_DETAIL_UI, "c", "Neuen Eintrag erstellen");
this.addTransition(UIStateEnum.READ_DETAIL_UI, "r", "Details anzeigen");
this.addTransition(UIStateEnum.UPDATE_DETAIL_UI, "u", "Eintrag ändern");
this.addTransition(UIStateEnum.DELETE_DETAIL_UI, "d", "Eintrag löschen");
*/
}
Damit das Gesamtsystem wieder in einem definierten Zustand ist, muss jetzt nur noch die Kontext-Klasse angepasst werden:
Anpassungen im Kontext
Gravierende Anpassungen ergeben sich auch am Kontext:
Zu Beginn muss jedes Zustandsobjekt einmal konfiguriert werden: die Methode
initializeTransitions();
wird daher in dermain()
-Methode für jedes Zustandsobjekt einmal aufgerufen.Danach wird das Kontext-Objekt erstellt,
der Ausgangszustand per
setUiStateEnum()
gesetzt undeine Methode
loop()
als Dauerschleife gestartet, die ab jetzt alle weitere Logik übernimmt.
public static void main(String[] args) {
for (UIStateEnum nextUiStateEnum : UIStateEnum.values()) {
nextUiStateEnum.getUIState().initializeTransitions();
}
UIContext myUI = new UIContext();
myUI.setUiStateEnum(UIStateEnum.ANMELDEN_UI);
for(;;){
myUI.loop();
}
}
In dieser loop()
wird
zunächst auf eine Eingabe auf der Konsole gewartet (
readln()
wird unten implementiert),geprüft, ob diese Zeichenkette einen Zustandswechsel hervorruft (die Methode
handleTriggerEvent()
delegiert dies an das aktive Zustandsobjekt) undder Zustandswechsel durchgeführt.
public void loop(){
String keyStroke = readln();
UIStateEnum uistate = handleTriggerEvent(keyStroke);
setUiStateEnum(uistate);
}
Die Methode handleTriggerEvent()
delegiert die Ausführung der Triggerauswertung - ganz wie im State-Pattern vorgesehen - an den aktiven Zustand:
public UIStateEnum handleTriggerEvent(String keyStroke){
return uiStateEnum.getUIState().handleTriggerEvent(keyStroke);
}
Die Methode readln()
schließlich gibt einfach die Eingabe in der Konsole zurück. Diese muss leider immer mit return
(Zeilenumbruch) abgeschlossen werden.
private String readln() {
String eingabe ="";
InputStreamReader inReader = new InputStreamReader(System.in);
try{
BufferedReader buffReader = new BufferedReader(inReader);
System.out.println("UI-Menu (Eingabe mit [ENTER] beenden)>"); //Eine Art Eingabeaufforderung - muss natürlich nicht sein
while ("".equals(eingabe)){
eingabe = buffReader.readLine();
}
} catch (IOException ex) {
System.out.println("Es ist ein Fehler aufgetreten: "+ ex.getMessage());
}
return eingabe;
}
Ab diesem Punkt ist das System wieder benutzbar, aber noch nicht allzu komfortabel. Man muss auswendig wissen, mit welchem Trigger man welchen Zustand erreichen kann. Mit drei kleinen Änderungen können wir unsere Konsolen-App noch mit einem Menü versorgen - dann ist das schon fast eine runde Sache.
Beim Eintritt in einen Zustand ein Menü mit allen möglichen Transitionen ausgeben
Wir haben schon fast alle Informationen zusammen, die wir benötigen, um uns ein Menü auszugeben:
Die Klasse Transitions
kennt alle zugelassenen Transitionen des aktiven Zustands - wir müssen sie nur noch mit einer neuen Methode aufbereiten:
public String getMenue(){
String menue = "";
for(Transition t : transitionList){
menue += t.toString()+"\n";
}
return menue;
}
Wenn uns Transitions
diese Einträge liefert, können wir sie in AbstractUIState
in einer neuen Methode aufbereiten und ausgeben:
public void showMenue(){
int menueheader = 60;
System.out.println();
System.out.println("#".repeat( menueheader));
int border = (menueheader - 2 - this.getClass().getSimpleName().length())/2;
System.out.println("#".repeat(border) + " "+this.getClass().getSimpleName() +" " + "#".repeat(border));
System.out.println("#".repeat( menueheader));
System.out.println(transitions.getMenue());
}
Wir müssen showMenue()
noch dem UIStateInterface
hinzufügen, damit wir das Menü aus dem UIContext
heraus erreichen können:
Die Methode showMenue()
können wir schließlich in entry()
des UIContext
aufrufen, damit wir bei jedem Eintritt in einen neuen Zustand direkt ein Menü erhalten:
Damit wir direkt nach Programmstart ein Menü erhalten (denn bis dahin hat ja noch ein Zustandsübergang stattgefunden) müssen wir einmal das Menü händisch aufrufen. Ich habe dazu in der main()
-Methode nach Setzen des Start-Zustands die Zeile myUI.uiStateEnum.getUIState().showMenue();
angefügt:
UIContext myUI = new UIContext();
myUI.setUiStateEnum(UIStateEnum.ANMELDEN_UI);
myUI.uiStateEnum.getUIState().showMenue();
Das Programm testen:
Wenn wir das Programm starten, meldet sich die Konsolenapp erwartungsgemäß mit einer Liste der möglichen Menüpunkte (Transitionen):
############################################################
######################## AnmeldenUI ########################
############################################################
'l': Anmelden (LIST_UI)
UI-Menu (Eingabe mit [ENTER] beenden)>
Eine Eingabe von l
löst wunschgemäß die Transition aus (wir haben noch relativ viele println
im Code, die das dokumentieren). Es wir das neue Menü ausgegeben.
Prüfe, ob der Trigger <l> eine Transition auslöst:
- Prüfe Transition LIST_UI mit Trigger <l>
==> Transition zu LIST_UI gefunden! (Weitere Suche wird gestoppt)
>>> Leaving state AnmeldenUI
<<< Entering state ListUI
############################################################
########################## ListUI ##########################
############################################################
'a': Abmelden (ANMELDEN_UI)
UI-Menu (Eingabe mit [ENTER] beenden)>
Dargestellt habe ich hier das Menü mit den beiden Zuständen AnmeldenUI
und ListUI
. Spätestens jetzt ist aber ein guter Zeitpunkt die übrigen Zustandsklassen und deren Transitionen anhand des oben abgebildeten UML-Zustandsdiagramms zu implementieren!
Das “Big Picture”
Am Ende der dritten Iteration ist schon relativ viel umgesetzt und wir haben eine funktionierende Navigation zwischen den unterschiedlichen Benutzerschnittstellen-Zuständen.
Das UML-Klassendiagramm des Gesamtsystems sieht mittlerweile schon recht umfangreich aus:

Was bleibt noch zu tun?
Um das komplette Zustandsdiagramm abzubilden und einen Zustandsautomaten zu implementieren fehlen aber noch einige Komponenten:
Transitionen bestehen aus Triggern, Guards und Effekten - bislang sind nur erstere implementiert
Trigger können bislang nur für Zustandswechsel vergeben werden: innere Aktivitäten können darüber noch nicht ausgelöst werden.
Innere Zustände, parallele Zustände… es gibt viel zu tun in der…
Hinweis zur Nachnutzung als Open Educational Resource (OER)
Dieser Artikel und seine Texte, Bilder, Grafiken, Code und sonstiger Inhalt sind - sofern nicht anders angegeben - lizenziert unter CC BY 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: “State Pattern-CLI: Realisierung von Transitionen und Menüstruktur” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/state-pattern-cli3 veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/design-pattern/state-pattern. Stand: 11.06.2023.
[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]