State Pattern-CLI: Effekte bei Zustandswechsel
https://bildung.social/@oerinformatik/110504024656041988
https://oer-informatik.de/state-pattern-cli4
tl/dr; (ca. 10 min Lesezeit): Für die Zustandsübergänge (Transitionen) unseres CommanLineInterface-Programms müssen noch Effekte (Aktionen) erstellt werden. Dabei müssen wir eine ganze Menge umbauen, um für Typsicherheit zu sorgen. (Zuletzt geändert am 11.06.2023)
Dieser Artikel ist Teil einer Serie:
- Aufbau des State-Patterns allgemein und grundlegende Struktur
- Realisierung der Zustände als Singletons (per Java-ENUMS)
- Realisierung der Transitionen und deren Trigger
- Realisierung von Effekten und Guards von Transitionen (dieser Artikel)
Grundstruktur
Wir werfen zu Beginn wieder einen Blick auf das UML-Zustandsdiagramm, mit dem die Kommandozeilen-Benutzeroberfläche modelliert wurde:

In der bisherigen Implementierung wurde bereits das Grundgerüst mit Transitionen erstellt, die Zustände in ENUMs organisiert und die nötigen Methoden erstellt, um ein Menü anzuzeigen und per Eingabe zwischen den Zuständen navigieren zu können.

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,eine abstrakte Klasse
AbstractUIState
, die gemeinsame Implementierungen aller Zustandsklassen umsetzt,die Klasse
Transition
implementiert einen einzelnen Zustandsübergang,in der Klasse
Transitions
werden einzelne Zustandsübergänge aggregiert und somit einem Zustand zugewiesen.
Darüber hinaus wurden Methoden in Kontext und Zustandsklassen integriert (z.B. showMenu()
), die der Menüerstellung dienen.
Was fehlt noch?
Wir haben bislang nur einen Teil der Transitionen umgesetzt: sie bestehen aus Triggern, Guards und Effekten. Bislang sind nur erstere implementiert. In dieser Iteration wollen wir uns um die Effekte kümmern.
Effekte von Transitionen
Bei jedem Zustandsübergang sollen Transitionen Aktivitäten ausführen: die Effekte. Diese Effekte sollen ein Verhalten modellieren, dass jedem Transitionsobjekt individuell übergeben wird. So soll z.B. im obigen Zustandsdiagramm beim Übergang von UpdateDetailsUI
zu ReadDetailUI
entweder die Methode abbruch()
oder die Methode ok()
ausgeführt werden - abhängig vom Trigger.
Effekte lassen sich mit Methoden darstellen, die auf dem übergebenen Objekt eine Methode aufrufen und keine Werte zurückgeben. In obigem Beispiel wird auf einer Instanz von UpdateDetailsUI
die Methode ok()
aufgerufen, die nach der Bestätigung das Speichern managed.
Da in Java Methoden nur innerhalb von Objekten existieren, müssen wir ein Objekt erzeugen, dessen einzige Aufgabe es ist, die Methode ui.ok()
aufzurufen.
class Aufrufklasse{
public void aufruf(UpdateDetailsUI ui){
ui.ok();
}
}
Aufrufklasse ark = new Aufrufklasse();
ark.aufruf(ui);
Das ist aber recht umständlich. Wir benötigen diese Klasse genau einmal für genau eine Instanz. Wir können das etwas abkürzen, in dem wir es als anonyme innere Klasse formulieren. Anonyme innere Klassen implementieren gegen ein Interface, das dann auch als Referenztyp fungiert. Java stellt hierfür das funktionale Interface Consumer zur Verfügung, das die abstrakte Methode void accept(Object t)
deklariert.
Consumer meinConsumer = new Consumer(){
public void accept(UpdateDetailsUI ui){
ui.ok();
}
};
meinConsumer.accept(ui);
Mit Lambda-Ausdrücken lässt es sich noch weiter vereinfachen (bei identischem Informationsgehalt):
Das Objekt, das wir hier meinConsumer
genannt hatten, übergeben wir jetzt an die Transition. Somit wird aus:
die Zeile mit Objektinstanziierung per Lambda-Ausdruck:
Bislang können wir aber dieses Objekt noch gar nicht entgegennehmen: addTransition()
kennt diesen Parameter noch nicht. Es gibt also einige Baustellen:
In den Transitionen muss ein
Consumer
als Attribut vorhanden sein, der den Effekt dieser Transition beinhaltet.Dieser Consumer sollte als Parameter typsicher jeweils die korrekte UI-Zustandsinstanz aufnehmen, schließlich wollen wir auf diesen Objekten Methoden des jeweiligen Typs aufrufen. Wir müssen also mit generischen Typen arbeiten.
Dieser Consumer muss bei der Zustandserstellung implementiert werden - alle Konstruktoraufrufe müssen also um den Parameter ergänzt werden.
Die Zustandsklassen müssen jeweils um die erforderlichen Effekte ergänzt werden - in die Methoden
initializeTransitions()
muss also für jede Transition ein Effekt angefügt werden.
Anpassungen in der Klasse Transition

Wir müssen zu jeder Transition einen Effekt speichern, dieser Methodenaufruf ist in einem Consumer gekapselt. Daher benötigen wir ein neues Consumer-Attribut und einen zugehörigen Getter. Im Konstruktor muss der Consumer gesetzt werden.
Da der Consumer Methoden der einzelnen Zustandsklassen aufruft, sollten wir ihn Typsicher implementieren. Wir nutzen dazu den Typparameter T
, der in den einzelnen Instanzen dann durch die jeweilige Klasse ersetzt wird. Über diese Generics bleiben wir flexibel, ohne die Typsicherheit zu opfern.
public class Transition<T>{
//...
private Consumer<T> effect;
public Consumer<T> getEffect() {return effect;}
public Transition(UIStateEnum target, String trigger, String menueItem, Consumer<T> effect){
//...
this.effect = effect;
}
Bei dem Consumer oben wurde per Generics spezifiziert, welchen Datentyp er konsumiert. Der Datentyp wird in spitzen Klammern übergeben, und überall da eingesetzt, wo ein Datentyp für den Consumer per Typparameter hinterlegt wurde.
Mit diesen Einfügungen passen aber alle Objekterzeugungen der Transitions
nicht mehr, da der Konstruktor nun den Consumer<T>
-Parameter benötigt:
Anpassungen in der Klasse Transitions
Auch die Klasse Transitions
sollten wir per Generics jetzt typsicher parametrisieren. Der Klassenkopf und die transitionsList
sollten mit dem Typparameter <T>
erweitert werden, damit sie für jede Instanz die zugrundeliegende Klasse als Referenz nutzt.
public class Transitions<T> {
private ArrayList<Transition<T>> transitionList = new ArrayList<>();
...
}
Die addTransitions()
-Methode muss eigentlich nur einen weiteren Parameter (effect
) vom generischen Typ Consumer<T>
an den Transition
-Konstruktor weiterreichen. Da die konkreten Zustandsklassen addTransition()
aber von AbstractUIState
erben, können wir hier gar keinen Wert für den Typparameter für <T>
übergeben. Wir nehmen also einen allgemeinen Consumer
entgegen, ohne Typparameter.
public void addTransition(UIStateEnum target, String trigger, String menueItem, Consumer effect){
transitionList.add(new Transition<T>(target, trigger, menueItem, effect));
}
Auch die Methode für den Zustandsübergang (Methode transist(Transition transition)
) wird aus AbstractUIState
aufgerufen, deshalt lassen wir auch hier den Parameter Transition
oder Generics (<T>
). Um den Effekt auszuführen, müssen wir
den Effekt (
Consumer<T>
) aus der Transition per Getter holen:transition.getEffect()
die
Consumer<T>
-Methode aufrufen:.accept(..)
als Parameter den aktuellen Zustand übergeben (denn auf diesem soll die Effekt-Methode aufgerufen werden):
source.getUIState()
den Parameter casten, also vom Typ
UIStateInterface
, den die ENUM-MethodegetState
zurückgibt, in den konkreten Typ der jeweiligen Zustandsklasse wandeln:(T) source.getUIState()
Im Ganzen also:
In der Implementierung erfolgt dies in Einzelschritten und mit einer Prüfung, ob ein Event vorhanden ist. Bevor wir den (nur der Abschnitt if (!(transition.getEffect() == null)){...}
ist neu). Damit der Compiler sich mit Warnungen zurückhält, weil wir Typen verändern, deaktivieren wir diese Warnungen per Annotation für die Methode (@SuppressWarnings("unchecked")
).
@SuppressWarnings("unchecked")
public UIStateEnum transist(Transition transition){
if ((transition!= null) && (transition.getTarget() != null)){
if (!(transition.getEffect() == null)){
Consumer<T> effect = transition.getEffect();
System.out.println("--- Führe Event auf " + source.name() + " aus: ---");
effect.accept((T) source.getUIState());
System.out.println("--- Event auf " + source.name() + " beendet ---");
}
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;
}
}
Auch in der Methode findTransition()
ergänzen wir in der Signatur und zu Beginn der for
-Schleife den Typparameter:
public Transition<T> findTransition(String actualTrigger){
System.out.println("Prüfe, ob der Trigger <"+actualTrigger+"> eine Transition auslöst:");
for(Transition<T> 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;
}
Anpassungen in der Klasse AbstractUIState
In der Klasse AbstractUIState
muss der Transitions
-Parameter typsicher formuliert werden, weil wir ihn oben generisch implementiert haben. Wir kennen in dieser Klasse, von der die konkreten Zustandsklassen erben, aber nicht den konkreten Typen und haben auch keinen Typparameter <T>
erhalten. Daher lassen wir uns die Transitions
-Objekte in den konkreten Zustandsklassen erzeugen (die kennen den Typen ja noch) und übernehmen die Objekte als Parameter.
Es ist jedoch sicher, dass die übergebenen Transitions
mit Implementierungen von UIStateInterface
operieren (schließlich haben das alle konkreten Zustandsklassen mittelbar implementiert). Wir nutzen als Ausdruck für die Generics also eine erweiterte Festlegung:
Das liest sich etwa: Das hier genutzte Transitions
-Objekte verfügt überall da, wo ein Typparameter steht über eine Instanz einer zuvor festgelegten Implementierung von UIStateInterface
. Der genutzte Datentyp ist also Superklasse oder implementiert das Interface des genannten Typs (hier UIStateInterface
). Wir müssen das sowohl beim Attribut transitions
als auch bei der Methode initializeTransitions()
ergänzen:
private Transitions<? extends UIStateInterface> transitions;
public void initializeTransitions(Transitions<? extends UIStateInterface> myTransisitons) {
transitions = myTransisitons;
}
Die gleiche Überlegung gilt analog für den Consumer - auch hier kennen wir in AbstractUIState
nicht die konkreten Datentyp, können ihn aber mit Consumer<? extends UIStateInterface>
eingrenzen. Wir übergeben den Effekt an die Transition:
public void addTransition(UIStateEnum target, String trigger, String menueItem, Consumer<? extends UIStateInterface> effect) {
this.transitions.addTransition(target, trigger, menueItem, effect);
}
Um abwärtskompatibel zu bleiben wird eine überladene Funktion erstellt, die ohne Consumer
-Parameter nutzbar ist:
public void addTransition(UIStateEnum target, String trigger, String menueItem) {
this.addTransition(target, trigger, menueItem, (o)->{System.out.println("Keine Aktivität für diesen Event hinterlegt.");});
}
Auch in der Methode handleTriggerEvent()
ergänzen wir den Typparameter:
public UIStateEnum handleTriggerEvent(String trigger) {
Transition<? extends UIStateInterface> foundTransition = this.transitions.findTransition(trigger);
return this.transitions.transist(foundTransition);
}
Anpassungen in den einzelnen Zustands-Klassen
Was jetzt noch fehlt, sind die Anpassungen in den konkreten Zustandsklassen. In der jeweiligen initializeTransitions()
müssen wir also ergänzen:
Transitions-Objekte müssen typsicher erstellt werden: Hier wird endlich festgelegt, welcher Datentyp verwendet wird. Wir rufen also die
initializeTransitions()
der Superklasse auf und übergeben ein neu erzeugtes Objekt, das festlegt, dass die Transitionen vom Typ der jeweiligen Instanz sind. In der KlasseListUI
also beispielsweise:jeder Transition einen zugehörigen Effekt anfügen, wenn dieser gewünscht ist. Wichtig dabei ist, dass wir auch im Lambda-Ausdruck den jeweiligen Typ der konkreten Klasse angeben. In der
ListUI
-Klasse also beispielsweise:
Das muss jeweils in den einzelnen Zustandsklassen erfolgen - für jede einzelne Transition. Wenn die einzelnen Transitionen dort schon eingefügt waren, müssen nur die Consumer
-Parameter übergeben werden. In der ListUI
ist es recht umfangreich und sieht (wenn alle Zustände implementiert sind) beispielsweise so aus:
public void initializeTransitions() {
super.initializeTransitions(new Transitions<ListUI>(UIStateEnum.getUIStateEnum(this)));
this.addTransition(UIStateEnum.ANMELDEN_UI, "a", "Abmelden", (ListUI aUI) -> {aUI.abbruch();});
this.addTransition(UIStateEnum.CREATE_DETAIL_UI, "c", "Neuen Eintrag erstellen", (ListUI aUI) -> {});
this.addTransition(UIStateEnum.READ_DETAIL_UI, "r", "Details anzeigen", (ListUI aUI) -> {});
this.addTransition(UIStateEnum.UPDATE_DETAIL_UI, "u", "Eintrag ändern", (ListUI aUI) -> {});
this.addTransition(UIStateEnum.DELETE_DETAIL_UI, "d", "Eintrag löschen", (ListUI aUI) -> {});
}
Kleinere Anpassungen im Kontext
In der Kontext-Klasse UIKontext
können jetzt beispielsweise noch zwei Anpassungen erfolgen:
- Ein Zustandswechsel mit Aufruf der exit- und entry-Aktivitäten erfolgt nur, wenn es sich tatsächlich um einen anderen als den bisherigen Zustand handelt:
public void setUiStateEnum(UIStateEnum newUiStateEnum) {
if (newUiStateEnum != this.uiStateEnum) {
this.uiStateEnum = newUiStateEnum;
}
}
- Der Ausgangszustand wird im Konstruktor des Kontexts gewählt:
public UIContext() {
this.setUiStateEnum(UIStateEnum.ANMELDEN_UI);
uiStateEnum.getUIState().entry();
}
Zum Schluss wieder: das Big Picture, aber was fehlt noch?

Neue Techniken kommen jetzt nicht mehr dazu, aber wir können vorhandenes noch variieren:
Guards lassen sich über Lambda-Ausdrücke (Predicate), die den Transitionen angehängt werden, realisieren
Trigger können bislang nur für Zustandswechsel vergeben werden: innere Aktivitäten können darüber noch nicht ausgelöst werden. Hierzu ist eine gesonderte Struktur nötig, die wir analog aufbauen können.
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: Effekte bei Zustandswechsel” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/state-pattern-cli4 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]