State Pattern-CLI: Effekte bei Zustandswechsel
https://bildung.social/@oerinformatik/110504024656041988
https://oer-informatik.de/state-pattern-cli4
State-Pattern, Zustandsmuster, Effekt, Transition, Consumer, Lambda-Ausdruck, anonyme innere Klasse, Generics, Typsicherheit, Java
tl/dr; (ca. 16 min Lesezeit): Für die Zustandsübergänge (Transitionen) unseres Command-Line-Interface-Programms müssen noch Effekte (Aktionen) erstellt werden. Dabei müssen wir eine ganze Menge umbauen, um für Typsicherheit zu sorgen [AI-Assisted]. (Zuletzt geändert am 28.10.2026)
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 bei Transitionen (dieser Artikel)
Schon fit in diesem Thema? Nutze die Kann-Liste und die Leitfragen unter dem Artikel als Checkliste, ob Du die relevanten Kompetenzen bereits hast.
Dieser Artikel baut auf der Kann-Liste der Artikel State-Pattern-CLI Teil 1 bis Teil 3 auf.
Als Grundlagen zu Effekten im State-Pattern kann ich nach Durchlesen des Artikels…
… erklären, was der Effekt einer Transition ist und wann er ausgeführt wird.
… erklären, wie in Java ein Verhalten als Objekt übergeben werden kann (anonyme innere Klasse, funktionales Interface
Consumer).… eine anonyme innere Klasse in einen Lambda-Ausdruck umformen.
… einen Effekt als
Consumeran eine Transition übergeben und beim Zustandsübergang ausführen.… generische Klassen mit einem Typparameter
<T>in Java deklarieren und nutzen.
Darüber hinaus kann ich als Expertenwissen…
… die Wildcard
<? extends UIStateInterface>erklären und einsetzen.… beurteilen, wo Raw Types und ungeprüfte Casts (
@SuppressWarnings("unchecked")) die Typsicherheit einschränken.
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
AnmeldenUIundListUI), die das Verhalten in bestimmten Zuständen implementieren,die
ENUM-Klasse (UIStateEnum), die die einzelnen Zustandsobjekte als Singletons zentral verwaltet,ein gemeinsames Interface
UIStateInterfaceder Zustandsklassen, das die Austauschbarkeit der Zustände ermöglicht,eine abstrakte Klasse
AbstractUIState, die gemeinsame Implementierungen aller Zustandsklassen umsetzt,die Klasse
Transition, die einen einzelnen Zustandsübergang implementiert,die Klasse
Transitions, in der einzelne Zustandsübergänge aggregiert und somit einem Zustand zugewiesen werden.
Darüber hinaus wurden Methoden in Kontext und Zustandsklassen integriert (z. B. showMenue()), 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, das jedem Transitionsobjekt individuell übergeben wird. So soll z. B. im obigen Zustandsdiagramm beim Übergang von UpdateDetailUI 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 UpdateDetailUI die Methode ok() aufgerufen, die nach der Bestätigung das Speichern übernimmt.
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(UpdateDetailUI 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, indem 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(T t) deklariert. Den Typ T geben wir in spitzen Klammern an (hier Consumer<UpdateDetailUI>).
Consumer<UpdateDetailUI> meinConsumer = new Consumer<UpdateDetailUI>(){
public void accept(UpdateDetailUI ui){
ui.ok();
}
};
meinConsumer.accept(ui);Mit Lambda-Ausdrücken lässt es sich noch weiter vereinfachen (bei identischem Informationsgehalt):
Consumer<UpdateDetailUI> meinConsumer = (UpdateDetailUI aUI) -> {aUI.ok();};
meinConsumer.accept(ui);Das Objekt, das wir hier meinConsumer genannt hatten, übergeben wir jetzt an die Transition. Somit wird aus:
this.addTransition(UIStateEnum.READ_DETAIL_UI, "o", "Ok, Speichern");die Zeile mit Objektinstanziierung per Lambda-Ausdruck:
this.addTransition(UIStateEnum.READ_DETAIL_UI, "o", "Ok, Speichern", (UpdateDetailUI aUI) -> {aUI.ok();});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
Consumerals 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 die Objekterzeugungen von Transition in der Klasse 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 addTransition()-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 <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, deshalb lassen wir auch hier den Parameter Transition ohne 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-MethodegetUIState()zurückgibt, in den konkreten Typ der jeweiligen Zustandsklasse wandeln:(T) source.getUIState()
Im Ganzen also:
transition.getEffect().accept((T) source.getUIState());In der Implementierung erfolgt dies in Einzelschritten und mit einer Prüfung, ob ein Effekt vorhanden ist (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:
Transitions<? extends UIStateInterface>Das liest sich etwa so: Das hier genutzte Transitions-Objekt arbeitet überall dort, wo ein Typparameter steht, mit einer zuvor festgelegten Implementierung von UIStateInterface. Der genutzte Datentyp ist also der genannte Typ selbst oder ein Untertyp davon, der ihn erweitert bzw. implementiert (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 den 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 Methode 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 Effekt 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 KlasseListUIalso beispielsweise:super.initializeTransitions(new Transitions<ListUI>(UIStateEnum.getUIStateEnum(this)));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:this.addTransition(UIStateEnum.ANMELDEN_UI, "a", "Abmelden", (ListUI aUI) -> {aUI.abbruch();});
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 UIContext können jetzt beispielsweise noch zwei Anpassungen erfolgen:
- Der Zustand wird nur neu gesetzt, 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.
Leitfragen
- Was ist der Effekt einer Transition?
Ein Effekt ist eine Aktivität, die beim Zustandsübergang ausgeführt wird – im Beispiel ok() oder abbruch() beim Übergang von UpdateDetailUI zu ReadDetailUI, abhängig vom Trigger. siehe Abschnitt „Effekte von Transitionen“
- Wie kannst Du in Java ein Verhalten als Parameter übergeben?
Da Methoden in Java nicht für sich allein existieren, wird ein Objekt übergeben, dessen Methode das Verhalten enthält. Kompakt geht das als anonyme innere Klasse, die das funktionale Interface Consumer<T> mit accept(T t) implementiert, oder noch kürzer als Lambda-Ausdruck. siehe Abschnitt „Effekte von Transitionen“
- Warum werden
TransitionundTransitionsmit einem Typparameter<T>versehen?
Der Effekt ruft Methoden der konkreten Zustandsklasse auf. Über den Typparameter wird festgelegt, welchen Typ der Consumer verarbeitet – so bleibt der Code typsicher, ohne für jede Zustandsklasse eigene Transitionsklassen schreiben zu müssen. siehe Abschnitt „Anpassungen in der Klasse Transition“
- Wie wird der Effekt beim Zustandsübergang ausgeführt?
In transist() wird der Consumer per getEffect() geholt und mit accept() aufgerufen; als Parameter dient das auf T gecastete Zustandsobjekt source.getUIState(). Das geschieht vor exit() und entry(); die Warnung zum ungeprüften Cast wird per @SuppressWarnings("unchecked") unterdrückt. siehe Abschnitt „Anpassungen in der Klasse Transitions“
- Was bedeutet der Typ
Transitions<? extends UIStateInterface>?
Die Wildcard steht für ein Transitions-Objekt, dessen Typparameter UIStateInterface oder ein Untertyp davon ist. AbstractUIState kennt den konkreten Typ nicht, kann ihn so aber eingrenzen. siehe Abschnitt „Anpassungen in der Klasse AbstractUIState“
- Wo wird der konkrete Typparameter schließlich festgelegt?
In initializeTransitions() der konkreten Zustandsklasse: Dort wird z. B. new Transitions<ListUI>(…) erzeugt, und die Lambda-Ausdrücke der Effekte geben den Parametertyp an, etwa (ListUI aUI) -> {aUI.abbruch();}. siehe Abschnitt „Anpassungen in den einzelnen Zustands-Klassen“
- Wie ließen sich Guards von Transitionen umsetzen?
Analog zu den Effekten: als Lambda-Ausdruck vom Typ Predicate, der der Transition angehängt und vor dem Zustandsübergang ausgewertet wird. siehe Abschnitt „Zum Schluss wieder: das Big Picture“
Links und weitere Informationen
Generics: Upper Bounded Wildcards im Java-Tutorial von Oracle
als Nachschlagewerk: Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides: Design Patterns. Elements of Reusable Object-Oriented Software, Addison-Wesley 1994 (Muster „State“)
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/entwurfsmuster. Erstellt mit KI-Assistenz; von mir umfassend redaktionell überarbeitet, geprüft und freigegeben. Stand: 28.10.2026.
[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]
