Das State Design Pattern

Studienprojekt von Philipp Hauer. 2009 - 2010. ©

Inhalt

Einführung

Wir wollen unsere Freundin (oder wahlweise den Freund) modellieren. Wer denkt, dies sei praxisfern, der irrt. Man denke nur an das erfolgreiche PC-Spiel "Die Sims". Die folgende Modellierung könnte dort realisiert worden sein.

Nun, wir können mit unserer Freundin interagieren: Wir können uns mit ihr unterhalten, ihr einen Kuss geben oder sie ärgern. Je nach ausgeführter Aktion wird sich ihre Laune (ihr Zustand) ändern: Neutral, Bockig oder Fröhlich. Küsst man eine neutralgelaunte Freudin, wird sie fröhlich. Ärgert man sie, wird sie bockig.

Die Zustände und die Zustandsübergänge unserer Freundin seien in folgendem Zustandsautomat illustriert:

Zustandsautomat Freundin: Einleitung State Design Pattern

Je nach Zustand wird die Freundin unterschiedlich auf Interaktionen (unterhalten(), verärgern(), kussGeben()) reagieren. Die Realität lehrt uns, dass es sich mit einer bockigen Freundin anders unterhält, als mit einer fröhlichen. Das äußere Verhalten der Freundin ändert sich also in Abhängigkeit von ihrem inneren Zustand.

Versetzen wir uns gedanklich in die 1970er Jahre: zur Blütezeit der Strukturierten Programmierung. Wie hätte man damals das Problem gelöst? Der aktuelle Zustand würde durch eine Integer-Variable repräsentiert werden. 0 steht für Neutral, 1 für Bockig und 2 für Fröhlich. In jeder Operation (unterhalten(), kussGeben(), verärgern()) wird zunächst geprüft, welchen Wert diese Integer-Variable hat und entsprechend wird ein Verhalten ausgeführt.

Einleitung State Design Pattern. Strukturierter Ansatz

Code der Klasse Freundin:

class Freundin{
    //Mögliche Launen der Freudin
    private static final int NEUTRAL = 0;
    private static final int BOCKIG = 1;
    private static final int FRÖHLICH = 2;
    
    //Zahl repräsentiert aktuellen Zustand
    private int aktuellerZustand;
    
    //Zustandsabhängiges Verhalten
    public void unterhalten(){
        if (aktuellerZustand == NEUTRAL){
            //NEUTRAL-spezifisches Verhalten...
            System.out.println("Fününününü.");
        } else if (aktuellerZustand == BOCKIG){
            //BOCKIG-spezifisches Verhalten...
            System.out.println("Fahr jetzt nach Hause! Ich will nicht mit dir reden!");
        } else if (aktuellerZustand == FRÖHLICH){
            //FRÖHLICH-spezifisches Verhalten...
            System.out.println("Hihi, Fünüüüüüüünü!");
        } 
    }
    public void kussGeben(){
        if (aktuellerZustand == NEUTRAL){
            //NEUTRAL-spezifisches Verhalten...
            System.out.println("Hihi :-)");
            aktuellerZustand = FRÖHLICH; //Zustandsänderung!
        } else if (aktuellerZustand == BOCKIG){
            //BOCKIG-spezifisches Verhalten...
            System.out.println("Na gut! Hab dich wieder lieb.");
            aktuellerZustand = NEUTRAL; //Zustandsänderung!
        } else if (aktuellerZustand == FRÖHLICH){
            //FRÖHLICH-spezifisches Verhalten...
            System.out.println("Hihi, :-D");
        } 
    }
    public void verärgern(){
        if (aktuellerZustand == NEUTRAL){
            //NEUTRAL-spezifisches Verhalten...
            System.out.println("Du spinnst wohl! Ich bin sauer! ;-(");
            aktuellerZustand = BOCKIG; //Zustandsänderung!
        } else if (aktuellerZustand == BOCKIG){
            //BOCKIG-spezifisches Verhalten...
            System.out.println("Du machst alles bloß noch schlimmer!");
        } else if (aktuellerZustand == FRÖHLICH){
            //FRÖHLICH-spezifisches Verhalten...
            System.out.println("Du spinnst wohl! ;-(");
            aktuellerZustand = BOCKIG; //Zustandsänderung!
        } 
    }
}			

Es funktioniert zweifellos. Jedoch hat es seinen Grund, dass die reine strukturierte Programmierung durch die Objektorientierung verdrängt wurde:

  • Unleserlichkeit und Unübersichtlichkeit. Der Code hat mit unserem Zustandsautomaten kaum etwas gemein. Das Verhalten für jeden Zustand ist über die Methoden verteilt. Man stelle sich nur den Entwurf mit 10 oder mehr Zuständen und 15 Methoden vor. Durch die Verteilung der Zustände sind weiterhin keine expliziten Zustandübergänge vorhanden.
  • Schlechte Wartbarkeit und Erweiterbarkeit. Sollen neue Zustände eingeführt werden, so muss umständlich jede Operation um einen weiteren else-Zweig erweitert werden. Die Fehlerträchtigkeit hierbei ist enorm.
    • Wie wäre es mit folgender neuen Anforderung: Um die Modellierung noch realistischer zu machen, soll mit einer Wahrscheinlichkeit von 10% ein Aufruf von unterhalten() im Zustand Neutral zu einem Wechsel in den Zustand Bockig führen, weil man(n) wieder irgendwas gesagt hat, was der Freundin missfällt (Gerne können Sie die Wahrscheinlichkeit auch höher wählen, je nach persönlicher Erfahrung. ;-) ). Zurück zur Sache: Es ist leicht vorstellbar, wie schnell solche zahlreichen kleinen und größeren Anforderungen und Fallunterscheidungen in der Summe den eh schon unübersichtlichen Code noch kryptischer machen. Ein Wartungsalptraum.

Durch diese strukturierte Denke verletzen wir zahlreiche OO-Entwurfsprinzipien:

 Identifiziere jene Aspekte, die sich ändern und trenne sie von jenen, die konstant bleiben.

Wahrscheinlich müssen in Zukunft neue Zustände integriert werden, die Freundin an sich bleibt dabei jedoch konstant. Bei unserem jetzigem Entwurf muss bei jeder Änderung der Code der Freundin angefasst werden.

Offen/Geschlossen-Prinzip (Open/Closed):
Entwürfe sollten für Erweiterungen offen, aber für Veränderungen geschlossen sein.

Diese Prinzip fordert letztlich das selbe: Der Freundincode soll für Veränderungen geschlossen sein, aber sie soll um neue Zustände jederzeit erweitert werden können.

Kohäsion (= Grad, inwiefern eine Klasse einen einzigen konzentrierten Zweck hat) und Delegation:
Kapsele Verantwortlichkeiten in eigenen Objekten und delegiere Aufrufe an diese Objekte. Es gilt: eine Klasse, eine Verantwortlichkeit.

Gerade das letzte Entwurfsprinzip macht eins deutlich: Die Zustände müssen in eigenen Objekten gekapselt werden.

Schauen wir uns doch noch einmal die Methoden unterhalten(), kussGeben() und verärgern() an. Sie haben alle eine ähnliche Struktur von Bedingungsanweisungen. Nun liegt es Nahe, alle Bedingungszweige, der logisch zu einem Zustand gehört, auch in ein gemeinsames Objekt (das Zustandsobjekt) zu übertragen. Damit enthält jedes entstandene Zustandsobjekt das Verhalten für diesen Zustand.

State Design Pattern: Analogie von Zustandsautomat und Klassendiagramm

Zum Vergleich: Alter Freundincode. Strukturgleiche Methoden mit Fallunterscheidungen:

public void unterhalten() {
    if (aktuellerZustand == NEUTRAL) {
        //NEUTRAL-spezifisches Verhalten...
        System.out.println("Fününününü.");
    }
    else if (aktuellerZustand == BOCKIG) {
        //BOCKIG-spezifisches Verhalten...
        System.out.println("Fahr jetzt nach Hause! Ich will nicht mit dir reden!");
    }
    else if (aktuellerZustand == FRÖHLICH) {
        //FRÖHLICH-spezifisches Verhalten...
        System.out.println("Hihi, Fünüüüüüüünü!");
    }
}

public void kussGeben() {
    if (aktuellerZustand == NEUTRAL) {
        //NEUTRAL-spezifisches Verhalten...
        System.out.println("Hihi :-)");
        aktuellerZustand = FRÖHLICH; //Zustandsänderung!
    }
    else if (aktuellerZustand == BOCKIG) {
        //BOCKIG-spezifisches Verhalten...
        System.out.println("Na gut! Hab dich wieder lieb.");
        aktuellerZustand = NEUTRAL; //Zustandsänderung!
    }
    else if (aktuellerZustand == FRÖHLICH) {
        //FRÖHLICH-spezifisches Verhalten...
        System.out.println("Hihi, :-D");
    }
}

public void verärgern() {
    if (aktuellerZustand == NEUTRAL) {
        //NEUTRAL-spezifisches Verhalten...
        System.out.println("Du spinnst wohl! Ich bin sauer! ;-(");
        aktuellerZustand = BOCKIG; //Zustandsänderung!
    }
    else if (aktuellerZustand == BOCKIG) {
        //BOCKIG-spezifisches Verhalten...
        System.out.println("Du machst alles bloß noch schlimmer!");
    }
    else if (aktuellerZustand == FRÖHLICH) {
        //FRÖHLICH-spezifisches Verhalten...
        System.out.println("Du spinnst wohl! ;-(");
        aktuellerZustand = BOCKIG; //Zustandsänderung!
    }
}			
Nun kapselt die drei Zustandsklassen Neutral, Bockig und Fröhlich das zustandsspezifische Verhalten der Freundin:

class Neutral {
    public void unterhalten() {
        //NEUTRAL-spezifisches Verhalten...
        System.out.println("Fününününü.");
    }
    public void kussGeben() {
        //NEUTRAL-spezifisches Verhalten...
        System.out.println("Hihi :-)");
        //Zustandsänderung zum Zustand "Fröhlich". Dazu später.
    }
    public void verärgern() {
        //NEUTRAL-spezifisches Verhalten...
        System.out.println("Du spinnst wohl! Ich bin sauer! ;-(");
        //Zustandsänderung zum Zustand "Bockig". Dazu später.
    }
}
class Bockig {
    public void unterhalten() {
        //BOCKIG-spezifisches Verhalten...
        System.out.println("Fahr jetzt nach Hause! Ich will nicht mit dir reden!");
    }
    public void kussGeben() {
        //BOCKIG-spezifisches Verhalten...
        System.out.println("Na gut! Hab dich wieder lieb.");
        //Zustandsänderung zum Zustand "Neutral". Dazu später.
    }
    public void verärgern() {
        //BOCKIG-spezifisches Verhalten...
        System.out.println("Du machst alles bloß noch schlimmer!");
    }
}
class Fröhlich {
    public void unterhalten() {
        //FRÖHLICH-spezifisches Verhalten...
        System.out.println("Hihi, Fünüüüüüüünü!");
    }
    public void kussGeben() {
        //FRÖHLICH-spezifisches Verhalten...
        System.out.println("Hihi, :-D");
    }
    public void verärgern() {
        //FRÖHLICH-spezifisches Verhalten...
        System.out.println("Du spinnst wohl! ;-(");
      //Zustandsänderung zum Zustand "Bockig". Dazu später.
    }
}			

Die Zustandsklassen haben die selbe Schnittstelle, wie die Freundin (unterhalten(), kussGeben(), verärgern()). Die Freundin aggregiert fortan ein solches Zustandsobjekt (= aktueller Zustand) und delegiert Aufrufe an dieses Zustandsobjekt. Dazu wird eine Schnittstelle (hier Interface IZustand) für die Zustände eingeführt.

Die "neue" Freundin: Aufruf-Delegation an das aktuell gesetzte Zustandsobjekt:

class Freundin {
    //Membervariable mit dem aktuellem Zustand
    private IZustand aktuellerZustand;

    //Die "neue" Freundin delegiert die Aufruf an ihren aktuellen Zustand
    public void unterhalten() {
        aktuellerZustand.unterhalten();
    }

    public void kussGeben() {
        aktuellerZustand.kussGeben();
    }

    public void verärgern() {
        aktuellerZustand.verärgern();
    }
}			

State Design Pattern Einleitung

Soweit, so gut. Bleibt nur eine Frage offen: Wie wechselt die Freundin ihre Zustände? Eine denkbar einfache Vorgehensweise ist dabei, das gewünschte Zustandsobjekt bei jedem Zustandswechsel neu zu instanziieren. Dies ist natürlich in unserem Fall (häufige Zustandswechsel) nicht sonderlich klug, jedoch sei es dennoch aus Gründen der Einfachheit und Didaktik realisiert. Die Problematik der Zustandswechsel wird unter Variationen näher diskutiert.

Damit die Zustandsobjekte selbstständig den Zustand der Freundin wechseln können, benötigen sie eine Referenz auf die Freundin. Weiterhin muss die Freundin um einen Setter zum Setzen des gewünschten Zustands erweitert werden und ein mit der Freundin parametrisierter Konstrukur für die Zustände definiert werden, damit die Zustände die Freundin kennen und den aktuellen Zustand der Freundin setzen können.

Der angepasste Entwurf sieht wie folgt aus:

State Design Pattern Einleitung

Freundin mit erweiterter Schnittstelle zum Setzen des Zustands:

class Freundin {
    //Setter zum Setzen des aktuellen Zustands.
    public void setAktuellerZustand(IZustand pAktuellerZustand){
        aktuellerZustand = pAktuellerZustand;
    }

    //Defaultzustand Neutral im Konstruktor setzen.
    public Freundin(){
        setAktuellerZustand(new Neutral(this));
    }
    
    //Rest wie gehabt.
    private IZustand aktuellerZustand;

    public void unterhalten() {
        aktuellerZustand.unterhalten();
    }

    public void kussGeben() {
        aktuellerZustand.kussGeben();
    }

    public void verärgern() {
        aktuellerZustand.verärgern();
    }
}			
Erweiterte Zustandsklassen, die selbstständig den Zustandswechsel vollziehen:

interface IZustand{
    public void unterhalten();
    public void kussGeben();
    public void verärgern();
}

class Neutral implements IZustand{
    //Konstruktur. Mit Freundin parametrisiert
    public Neutral(Freundin pFreundin){
        _freundin = pFreundin;
    }
    //Referenz auf die Freundin
    private Freundin _freundin;
    
    public void unterhalten() {
        System.out.println("Fününününü.");
    }
    public void kussGeben() {
        System.out.println("Hihi :-)");
        _freundin.setAktuellerZustand(new Fröhlich(_freundin)); //Zustandsübergang
    }
    public void verärgern() {
        System.out.println("Du spinnst wohl! Ich bin sauer! ;-(");
        _freundin.setAktuellerZustand(new Bockig(_freundin)); //Zustandsübergang
    }
}
class Bockig implements IZustand{
    //Konstruktur. Mit Freundin parametrisiert
    public Bockig(Freundin pFreundin){
        _freundin = pFreundin;
    }
    //Referenz auf die Freundin
    private Freundin _freundin;
 
    public void unterhalten() {
        System.out.println("Fahr jetzt nach Hause! Ich will nicht mit dir reden!");
    }
    public void kussGeben() {
        System.out.println("Na gut! Hab dich wieder lieb.");
        _freundin.setAktuellerZustand(new Neutral(_freundin)); //Zustandsübergang
    }
    public void verärgern() {
        System.out.println("Du machst alles bloß noch schlimmer!");
    }
}
class Fröhlich implements IZustand{
    //Konstruktur. Mit Freundin parametrisiert
    public Fröhlich(Freundin pFreundin){
        _freundin = pFreundin;
    }
    //Referenz auf die Freundin
    private Freundin _freundin;
 
    public void unterhalten() {
        System.out.println("Hihi, Fünüüüüüüünü!");
    }
    public void kussGeben() {
        System.out.println("Hihi, :-D");
    }
    public void verärgern() {
        System.out.println("Du spinnst wohl! ;-(");
        _freundin.setAktuellerZustand(new Bockig(_freundin)); //Zustandsübergang
    }
}		

Besonders schön lässt sich unser Entwurf nun durch den Client verwenden. Der Client hat keine Kenntnis von den Zuständen und Zustandswechsel der Freundin. Er kennt allein das Freundinobjekt und interagiert ausschließlich mit diesem. Dabei wechselt die Freundin zur Laufzeit dynamisch ihr Verhalten, ohne dass der Client davon etwas mitbekommt oder gar etwas dazu beiträgt. Er scheint so, als hätte die Freundin ihre Klasse geändert.

Benutzung durch den Client:

Freundin freundin = new Freundin();
//Defaultzustand: Neutral
freundin.unterhalten(); //Fününününü.
freundin.verärgern();   //Du spinnst wohl! Ich bin sauer! ;-(
//Ab jetzt: Bockig
freundin.unterhalten(); //Fahr jetzt nach Hause! Ich will nicht mit dir reden!
freundin.unterhalten(); //Fahr jetzt nach Hause! Ich will nicht mit dir reden!
freundin.kussGeben();   //Na gut! Hab dich wieder lieb.
//Ab jetzt: Neutral
freundin.kussGeben();   //Hihi :-)
//Ab jetzt: Fröhlich
freundin.unterhalten(); //Hihi, Fünüüüüüüünü!			

Voila! Unsere neue Freundin kann ihr Verhalten dynamisch in Abhängigkeit von ihrem inneren Zustand verändern. Damit bringt sie zahlreiche Vorzüge mit sich:

State Design Pattern: Intuivite Abbildung der Zustände

  • Verständlichkeit und Leserlichkeit. Die Abbildung eines jeden Zustands des Zustandsautomaten auf eine eigene Klasse ist äußerst intuitiv und verständlich.
  • Explizite Zustandsübergänge. Ebenso werden die Zustandswechsel explizit durch Setzen einer Variablen (Member "aktuellerZustand" der Freundin) durchgeführt. Dies ist keine kryptische Integer-Variable mehr (bei der die Leserlichkeit nur durch zusätzliche Konstanten nicht vollends verloren geht), sondern ein Zustandsobjekt. Sehr intuitiv und realitätsnahe. Das ist OO.
  • Weiterhin werden inkonsistente Zustände verhindert, die hätten auftreten können, wenn der Zustand der Freundin ursprünglich von mehreren Integer-Variablen abhängig gewesen wäre.
  • Hohe Kohäsion/Trennung der Verantwortlichkeiten. Das Wissen, um das zustandsspezifische Verhalten ist über mehrere Zustandsklassen aufgeteilt. Die Verantwortlichkeiten sind klar getrennt. Der Code einer jeden Klasse (Freundin, Zustände) wird schlank.
  • Hohe Änderungsstabilität und Erweiterbarkeit. Weiterhin führen Änderungen an den Zuständen oder die Integration neuer Zustände lediglich zur geringen Änderungsaufwand. Die Freundinklasse bleibt davon unangetastet (Freundin für Veränderung geschlossen, aber durch neue Zustände erweiterbar -> Offen/Geschlossen-Prinzip). Alles im Allem entsteht eine gute Wartbarkeit des Systems.
  • Wiederverwendbarkeit. Die Zustände sind modular und können auch von anderen Objekten neben der Freundin (z. B. Freund, Mutter, Bruder, Dozent, Polizist etc.) verwendet werden.
  • Der Entwurf ist (gerade hinsichtlich später Änderungen) weniger fehlerträchtig als breite, sich wiederholende if-else-Konstrukte.

Es zeigt sich, dass durch den Einsatz des State Patterns, ein hohes Maß an Flexibilität und Dynamik gewonnen wird, während zeitgleich die Wartung erleichtert wird und Erweiterungen schnell und unkompliziert möglich sind.

Nach dieser Einführung wird im folgenden Abschnitt das State Design Pattern formalisiert, näher analysiert und diskutiert.

Das Freundinbeispiel mit State Pattern Termini

Klasse State Teilnehmer
Freundin Kontext (Context)
Neutral, Bockig, Fröhlich Konkrete Zustände (Concrete States)

Analyse und Diskussion

Gang Of Four-Definition

State:
"Ermögliche es einem Objekt, sein Verhalten zu ändern, wenn sein interner Zustand sich ändert. Es wird so aussehen, als ob das Objekt seine Klasse gewechselt hat."
([GoF], Seite 398)

Beschreibung

State Design Pattern Beschreibung

Das State Entwurfsmuster ermöglicht die elegante Modellierung von zustandsabhängigen Verhalten eines Objekts. Je nach internen Zustand ändert sich das Verhalten des Objekts.

Es wird eine einheitliche Schnittstelle für die möglichen Zustände definiert (IState). Für jeden Zustand wird eine Klasse erstellt, die diese Schnittstelle realisiert (ConcreteStateX).

Das Objekt, dessen Verhalten in Abhängigkeit vom Zustand geändert werden soll (das Context-Objekt) aggregiert nun ein solches Zustandsobjekt (via Instanzvariable). Dieses Objekt repräsentiert den aktuellen internen Zustand und kapselt das zustandsabhängige Verhalten des Contexts. Der Context delegiert Aufrufe an sein aktuell gesetztes Zustandsobjekt.

Die Zustandswechsel und -übergänge können durch die konkreten Zustände selbst durchgeführt werden, in dem sie dem Context einen Folgezustand zu weisen. Mehr dazu unter Variationen.

State Design Pattern Beschreibung UML

Context:

class Context{
    private IState state;
    public void operate(){
        state.operate();
    }
}			
IState-Interface und Implementierungen:

interface IState{
    public void operate();
}
class ConcreteState1 implements IState{
    public void operate() {
        System.out.println("ConcreteState1");
        //ggf. Zustandswechsel, dazu Handle auf Context mit entsprechenden Setter notwendig
    }
}
class ConcreteState2 implements IState{
    public void operate() {
        System.out.println("ConcreteState2");
        //ggf. Zustandswechsel, dazu Handle auf Context mit entsprechenden Setter notwendig
    }
}
class ConcreteState3 implements IState{
    public void operate() {
        System.out.println("ConcreteState3");
        //ggf. Zustandswechsel, dazu Handle auf Context mit entsprechenden Setter notwendig
    }
}			

Variationen

Definition der Zustandsübergänge

Es muss definiert werden, wo entschieden wird, welcher Zustand als nächstes gesetzt werden soll. Dies kann zu einem in den konkreten Zustandsobjekten, aber auch im Kontextobjekt geschehen.

Zustände bestimmen Folgezustand

Damit jeder Zustand seinen Folgezustand bestimmen kann, muss er diesem beim Contextobjekt setzen. Dazu benötigt er aber

  • eine Referenz auf das Contextobjekt.
  • Weiterhin muss die Schnittstelle des Contextobjekts um entsprechende Setter erweitert werden.

Damit die Zustandsobjekte nicht immer wieder mit new neu instanziiert werden müssen, kann der Context alle möglichen Zustände als Attribute halten und Zugriff über Getter ermöglichen. Dies ist ein Versuch die Abhängigkeiten zwischen den Zustandsobjekten zu reduzieren.

State Design Pattern

Contextobjekt stellt alle nötigen Schnittstellen bereit, damit Zustände Zugriff auf den aktuellen Zustand haben und diesen neu setzen können:

class Context{
    private IState _currentState;
    public void operate(){
        _currentState.operate();
    }
    //Setter zum Setzen des aktuellen Zustands
    public void setState(IState pState){
        _currentState = pState;
    }
    
    //Context hält alle möglichen Zustände vor
    private IState _concreteState1;
    private IState _concreteState2;
    private IState _concreteState3;
    
    //Getter für die möglichen Zustände
    public IState getConcreteState1() {
        return _concreteState1;
    }
    public IState getConcreteState2() {
        return _concreteState2;
    }
    public IState getConcreteState3() {
        return _concreteState3;
    }
    
    //Initialisierung in Konstruktor
    public Context(){
        _concreteState1 = new ConcreteState1(this);
        _concreteState2 = new ConcreteState2(this);
        _concreteState3 = new ConcreteState3(this);
        _currentState = _concreteState1;
    }
}			
Zustände setzen Folgezustand selbst:

_context.setState(_context.getConcreteState3());			
Vollständige Code der Zustände:

interface IState{
    public void operate();
}
class ConcreteState1 implements IState{
    //Zustand muss Context zum Setzen des Folgezustands kennen.
    private final Context _context;

    //Konstruktor
    public ConcreteState1(Context pContext) {
        _context = pContext;
    }

    public void operate() {
        System.out.println("ConcreteState1");
        //Zustandswechsel, ggf. mit Bedingung
        _context.setState(_context.getConcreteState2());
    }
}
class ConcreteState2 implements IState{
    //Zustand muss Context zum Setzen des Folgezustands kennen.
    private final Context _context;
    
    //Konstruktor
    public ConcreteState2(Context pContext) {
        _context = pContext;
    }
    
    public void operate() {
        System.out.println("ConcreteState2");
        //Zustandswechsel, ggf. mit Bedingung
        _context.setState(_context.getConcreteState3());
    }
}
class ConcreteState3 implements IState{
    //Zustand muss Context zum Setzen des Folgezustands kennen.
    private final Context _context;
    
    //Konstruktor
    public ConcreteState3(Context pContext) {
        _context = pContext;
    }
    
    public void operate() {
        System.out.println("ConcreteState3");
        //Zustandswechsel, ggf. mit Bedingung
        _context.setState(_context.getConcreteState1());
    }
}			

Eine Alternative wäre es, die Zustände als Singletons zu realisieren. Damit müsste der Context nicht alle möglichen Zustände und die dafür notwendigen Getter bereitstellen. Dies ist aber nur möglich, wenn die Zustandsobjekte über keine Attribute und Daten verfügen. Eine andere Lösung wäre, die Zustände immer erst bei Bedarf zu erzeugen und danach gleich wieder zu löschen, so dass keine Referenz auf diese gehalten werden muss.

Durch diese Vorgehensweise wird der Entwurf sehr flexibel und dynamisch. Zustandswechsel können beliebig durchgeführt werden. Der Context ist von dieser Verantwortung befreit. Somit können Änderungen an den Zustandsübergängen durchgeführt werden, ohne den Context modifzieren zu müssen.

Context bestimmt Folgezustände

Die Verantwortung für die Zustandsübergänge beim Contextobjekt zu implementieren ist sinnvoll, wenn sich (einmal implementiert) an den Zustandswechsel kaum etwas ändert. Damit werden die Zustände schlanker, da sie sich nicht mehr um die Zustandsübergänge kümmern müssen. Weiterhin werden ihre Abhängigkeiten geringer, da sie das Contextobjekt und andere Zustände nicht mehr kennen müssen. Allerdings ist diese Variante starrer und unflexibler, wenn doch außerordentliche Zustandswechsel notwendig werden.

Folgezustand als Rückgabewert von operate()

Eine besonders interessante Alternative zur Bestimmung der Folgezustände ist folgende: Die operate()-Methode liefert den Folgezustand an den Context zurück. Dadurch kann jeder Zustand seinen Folgezustand selbst bestimmten und der Context ist von dieser Logik entbunden. Dieser Ansatz funktioniert allerdings nur, wenn aus der Programmlogik heraus, die operate()-Methode keinen anderen Rückgabewert haben muss. Ist dies der Fall (z. B. wenn operate() das Ergebnis einer Rechnung zurückliefert), kann dieser Ansatz nicht realisiert werden.

Lebenszeit von Zustandsobjekten

Erzeugung der Zustandsobjekts bei Bedarf Erzeugung der Zustandsobjekte im Voraus

Zustandsobjekte werden erst bei Bedarf instanziiert und nach erfolgter Nutzung sofort wieder gelöscht.

  • Dies ist sinnvoll, wenn sich der Zustand des Contexts nur selten ändert.
  • Es ist nicht mehr notwendig, dass der Context eine Referenz auf die möglichen Zustandsobjekte hält.

Alle möglichen Zustände werden in Voraus instanziiert und nie gelöscht.

  • Das ist sinnvoll und performant, wenn es häufig zu Zustandswechsel kommt. Es werden keine Objekte unnötig gelöscht, die in naher Zukunft eh wieder gebraucht werden.
  • Einmalig entstehen die Kosten zur Instanziierung der Zustandsobjekte.
  • Kein Kosten für das Zerstören von Zustandsobjekten
  • Allerdings muss dazu das Contextobjekt zu jedem möglichen Zustandsobjekt eine Referenz halten.

Gemeinsame Nutzung von Zustandsobjekten

Wenn die Zustände keinen internen Zustand (also Daten, Membervariablen) haben, so kann zur Einsparung von Ressourcen verschiedenen Contextobjekte die selben Zustandsobjekte nutzen. In diesem Fall könnte man die Realisierung des Singleton Design Pattern für die Zustände in Betracht ziehen.

Anwendungsfälle

Das State Design Pattern findet in folgenden Fällen Anwendung:

  • Ein Objekt soll sein äußeres Verhalten zur Laufzeit in Abhängigkeit von seinen Zustand ändern.
  • Ein Objekt besitzt ein Reihe von Methoden ähnlicher Struktur, die sich aus immer gleichen Bedingungsanweisungen zusammensetzen. Die Bedingungsanweisung prüft dabei den Wert einer Membervariablen (Integer, Enumeration), die den aktuellen Zustand repräsentiert.
    Objekt mit strukturähnlichen Operationen:
    
    public class ObjectWithoutStateDP {
        //Mögliche "Zustände"
        private static final int STATE_A = 0;
        private static final int STATE_B = 1;
        private static final int STATE_C = 2;
        
        //Zahl repräsentiert aktuellen "Zustand"
        private int _currentState;
        
        //Methoden ähnlicher Struktur
        //Achtung: Strukturierte Denke!
        public void go(){
            if (_currentState == STATE_A){
                //Zustand-A-spezifisches Verhalten...
            } else if (_currentState == STATE_B){
                //Zustand-B-spezifisches Verhalten...
            } else if (_currentState == STATE_C){
                //Zustand-C-spezifisches Verhalten...
            } 
        }
        public void stop(){
            if (_currentState == STATE_A){
                //Zustand-A-spezifisches Verhalten...
            } else if (_currentState == STATE_B){
                //Zustand-B-spezifisches Verhalten...
            } else if (_currentState == STATE_C){
                //Zustand-C-spezifisches Verhalten...
            } 
        }
        public void exit(){
            if (_currentState == STATE_A){
                //Zustand-A-spezifisches Verhalten...
            } else if (_currentState == STATE_B){
                //Zustand-B-spezifisches Verhalten...
            } else if (_currentState == STATE_C){
                //Zustand-C-spezifisches Verhalten...
            } 
        }
    }					

    Nach dem State Design Pattern wird für jedem Zweig der Bedingungsanweisung, die bei einen bestimmten internen Zustand ausgeführt werden, eine eigene Klasse erstellt - die Zustandsklasse. Alles was noch zu tun ist, ist den Methodenaufruf an eine Instanz dieser Zustandsklasse zu delegieren.

Konkrete Anwendungsfälle:

  • Werkzeuge in Zeichentools. In vielen Zeichentools kann der Benutzer zwischen Werkzeugen wählen. Das Auswahlwerkzeug ermöglicht das Ziehen von Rechtecken auf der Zeichenfläche, um alle Objekte in diesem Bereich auszuwählen. Mit dem Pinselwerkzeug können Striche gemalt werden und das Verlaufswerkzeug ermöglicht schöne Verläufe. Nun erscheint es dem Benutzer so, als würde er ein Werkzeug "packen" und "benutzen". Nun, tatsächlich ändert sich lediglich der Zustand der Zeichenfläche bzw. des Editors. Führte ein Klick in die Zeichenfläche im Zustand "Pinselwerkzeug" noch zur Erstellung eines Punktes, so führt der selbe Klick im Zustand "Füllwerkzeug" zum Ausfüllen der gesamten Zeichenfläche mit einer bestimmten Farbe. Das Verhalten des Editors bzw. seine Reaktion auf Events (Mausklick) ändert sich in Abhängigkeit zu seinen Zustand. Der Zustand wird durch die Auswahl eines Werkzeugs gesetzt.
    Anendung State Design Pattern. Werkzeuge sind Zustände
    Jedes Werkzeug setzt den Zustand des Editors
  • Zustandsautomaten. Natürlich können alle möglichen Zustandsautomaten (beispielsweise DEAs) mit Hilfe des State Entwurfsmusters elegant abgebildet werden.
    State Design Pattern: Anwendung. Zustandsautomat. DEA. Akzeptor
    Zustandsmaschinen (hier ein DEA/Akzeptor) können mit dem Zustand Entwurfsmuster abgebildet werden.
    • Parser. Die Funktionsweise vieler Textparser ist zwangsläufig zustandsbasiert. So muss ein Compiler, der Quellcode parst, ein Zeichen abhängig von den zuvor gelesen Zeichen interpretieren. So handelt es sich in vielen Programmiersprachen bei allen Zeichen, die einem "/*" folgen, um Kommentare bis zu einem "*/" (--> Kommentarzustand).
  • Repräsentation von Zuständen von (Netzwerk)Verbindungen. [GoF] (Seite 399) beschreibt die Modellierung einer TCP-Verbindung mit den Operationen open(), close(), confirm(). Das Verhalten dieser Methoden ist jedoch abhängig von dem Zustand der TCP-Verbindung: Etabliert, Lauschend oder Geschlossen.

Vorteile

  • Erweiterbarkeit und Änderungsstabilität. Neue Zustände können einfach ins System integriert werden, ohne dabei bestehenden Code ändern zu müssen. Änderungen am zustandsabhängigen Verhalten betreffen lediglich eine Zustandsklasse und nicht den Context.
  • Intuitivität und Verständlichkeit. Durch die hohe Kohäsion und der Delegation von Verantwortlichkeiten (das Verhalten eines Zustands ist im entsprechenden Zustandsobjekt selbst gekapselt/lokalisiert) wird der Code leicht verständlich. Das mannigfaltige Verhalten eines Objekts wird auf verschiedene Zustandsobjekte verteilt.
  • Das äußere Verhalten eines Objekts (des Contexts) kann beliebig geändert werden. Für den Client erscheint es so, als würde er es mit einem neuen Objekt einer anderen Klasse zu tun haben.
  • Explizite Zustandsübergänge. Durch die Einführung von Objekten für jeden Zustand wird der Zustandswechsel explizit. Außerdem werden inkonsistente Zustände verhindert, da ein Zustandswechsel ein atomarer Befehl ist (Setzen einer Variablen), im Gegensatz zum Binden mehrerer Membervariablen (Zahlen oder Enumerations) beim strukturierten Ansatz (häufig sind es mehrere).
  • Der Entwurf ist (gerade hinsichtlich späterer Änderungen) weniger fehlerträchtig als breite, sich wiederholende if-else-Konstrukte.

Nachteile

  • Erhöhte Klassenanzahl.
  • Weniger kompakt als eine einzige Klasse. Allerdings ist dies gerade das Ziel des Entwurfsmusters: Die Aufteilung des Verhaltens von einer einzigen unübersichtlichen Klasse auf mehrere Zustandsobjekte.

Exkurs: Vergleich zwischen State und Strategy Design Pattern

Beim Vergleich des State mit dem Strategy Design Pattern fällt eins sofort auf: beide haben ein identisches Klassendiagramm! Dennoch gibt es große Unterschiede in der Verwendung und der Absicht, die hinter den Pattern stecken, obwohl sie die gleiche Struktur haben.

State Strategy
State Design Pattern. Vergleich mit Strategy Strategy Design Pattern. Vergleich mit State
Gemeinsamkeit: Beide Pattern kapselt Verhalten in einem separaten Objekt. Der Context delegiert Aufrufe an dieses Objekt. Damit kann das Verhalten des Objekts flexibel (und auch zur Laufzeit) durch Setzen eines anderen Verhaltensobjekts geändert werden.
  • Kapselung zustandsbasiertem Verhalten und Delegation des Verhaltens an den aktuellen Zustand.
  • Client hat oft keine Kenntnis von den Zuständen. Die Zustandswechsel werden intern und für den Client unsichtbar durchgeführt. Daher entsteht der Eindruck, der Context gehöre plötzlich einer anderen Klasse an, weil sich sein Verhalten ohne Einflussnahme des Clients geändert hat.
  • Context oder Stateobjekt wechseln selbstständig den aktuellen Zustand. Der Zustandswechsel gehört mit zum Konzept des State Design Pattern.
  • Ein Contextobjekt wechselt zur Laufzeit sehr häufig seine Zustandsobjekte.
  • Das State Design Pattern bietet eine Alternative zu einer großen Menge von strukturähnlichen Bedingungsanweisungen in Methoden.
  • Unterklassen entscheiden, wie Verhalten implementiert wird.
  • Der Client bestimmt häufig initial die zu verwendende Strategie (Algorithmus) und setzt das entsprechende Strategyobjekt einmalig selbst.
  • Der Context oder das Strategyobjekt wechseln nicht selbstständig die aktuelle Strategie.
  • Häufig ist es ein Strategyobjekt das für ein Contextobjekt am besten passt und auch zur Laufzeit nicht mehr geändert wird.
  • Das Strategy Design Pattern bietet eine flexible Alternative zur Unterklassenbildung.
Ergo: Gleiche Struktur, aber unterschiedliche Absicht.

Kommentare

Bitte auswählen:*
Don 2016-06-03 10:40:45
Hey Philipp!
Erst einmal danke für das geniale Tutorial. Sehr informativ und einprägsam.
Du hast bei deinen Beispielen die Verwendung im Parser erwähnt.
Ich muss gerade einen Parser für SQL schreiben, bei dem jedes Statment über ein eigenes Objekt repräsentiert wird. Ein Befehl würde sich also dann nach dem Komposite-Pattern aus Pointern auf mehrere andere zusammensetzen.

Meine Frage ist nun, wie könnte ich deiner Meinung nach in dem Fall das State/Strategy - Pattern einsetzen? :D
Wäre echt toll, wenn du mir helfen könntest. Gerne auch per Mail ;)

LG
Christian 2015-08-28 08:17:23
Hmm, das habe ich mir auch gerade überlegt.

Allerdings müsste man dann in den Methoden "unterhalten/verärgern/küsse n" eine Switch-case Anweisung einfügen, die je nach aktuellem Zustand die richtige Reaktion ausführt. Also wenn ich die Freundin küsse wenn sie verärgert ist oder wenn sie eh schon fröhlich ist, macht ja einen Unterschied.
Peter Meier 2015-04-25 23:27:25
Wenn man C kann schreibt man:
typedef enum {neutral, froehlich, bockig} Zustand;
Zustand aktuell;
Zustand reaktion[3][3]=
{ /* unterhalten, veraergern, kuessen */
/* neutral */ { neutral, bockig, froehlich },
/* froehlich */ { froehlich, bockig, froehlich },
/* bockig */ { neutral, bockig, neutral }
};
void unterhalten() { aktuell = reaktion[aktuell][0]; };
void veraergern() { aktuell = reaktion[aktuell][1]; };
void kuessen() { aktuell = reaktion[aktuell][2]; };
Auf einen Blick übersichtlich und sehr änderungsfreundlich und 10 mal effizienter auszuführen.
Ali 2014-12-05 13:06:06
Wasch für design pattern?
Gerhard 2014-12-05 13:05:04
Danke für deinen Beitrag!
Alex 2014-10-21 10:35:42
Ich möchte mich für deine tolle Arbeit hier bedanken! Diese ganzen Pattern werden von dir sehr verständlich erklärt - für Leute die da einfache Anschauungsbeispiele benötigen eine super Sache.

Danke!
Uwe Reinersmann 2012-11-21 13:47:30
Hallo,
Super Seite, besonders die Codebeispiele, sehr hilfreich
Viele Grüße, weiter so.
Rob 2012-07-26 01:16:56
Hi Philipp,

sehr schön erklärt.
Ich bin gerade am Überlegen, ob ich eine Problemstellung per \"State\" am
Besten lösen könnte. Habe das mit einem Kollegen diskutiert, der State immer
für einen langen switch-case-Block gehalten hat.
Nun habe ich nach dem Lesen dieses guten Artikels die Bestätigung, dass \"State\" das
meint, was ich darunter verstanden hatte, hurrraaa!
Ich werd es wohl bei meiner Problemstellung anwenden.

Danke für den echt guten Artikel.

Grüße
Rob
Philipp 2012-07-13 17:51:28
Hey Patrick,

das stimmt. Vielen Dank für deinen Hinweis. :-)

Philipp
Patrick 2012-07-13 09:26:44
Alles sehr gut verständlich. Habe jedoch einen Fehler entdeckt: In Bild einleit3-kl.png und einleit3-gr.png ist der Konstruktor des Zustandes Fröhlich falsch angegeben.
Markus 2012-06-15 14:50:52
Exzellent! Sehr gut verständlich - auch für Programmierer mit weniger OOP-Erfahrung und sehr gute Geschichte. Danke für die Mühe!!!
Philipp 2011-12-07 10:13:59
Hallo Karl Heinz,

das State Pattern zielt auf eine Kapselung des Zustandswechsels ab. Der Client soll sich ja gerade nicht damit beschäftigen müssen, dass die Freundin verschiedenen Zustände hat... schon gar nicht darauf zugreifen (so weit die Definition). Transparenz. Vlt. möchte man das ja auch nicht, weil nur das Subjekt/die Zustände entscheiden sollen, wann der Zustand gewechselt wird. Vlt. führt ja ein verärgern() nicht immer zu einem Zustandswechsel?

Ziel von Patterns sind häufig eine einfache und intuitive Bedienung/API durch den Client:

//Defaultzustand: Neutral
freundin.unterhalten(); //Fününününü.
freundin.verärgern(); //Du spinnst wohl! Ich bin sauer! ;-(
//Ab jetzt: Bockig

Definition State: \"Ermögliche es einem Objekt, sein Verhalten zu ändern, wenn sein interner Zustand sich ändert. Es wird so aussehen, als ob das Objekt seine Klasse gewechselt hat.\"
Das ist das Ziel des State Patterns. Das Zugänglichmachen des Zustandskonzepts für den Client, mag eine Erweiterung sein, die in bestimmten Fällen sinnvoll ist, aber ist auch mit Nachteilen verbunden und entspricht nicht dem klassischen Pattern.

Viele Grüße
Philipp
Karl Heinz 2011-12-06 19:47:38
Hi,
ich finde die Geschichte richtig gut...einzig ein Detail könnte man (meiner Meinung nach) etwas schöne machen und zwar: Die \"Freundin\" implementiert nicht mehr die Zustände sondern man geht einfach über den getter von Freundin wie folgt:

Freundin f = new Freundin();
f.getZustand().verärgern();
f.getZustand().verärgern();
f.getZustand().unterhalten();
f.getZustand().kussGeben();
f.getZustand().unterhalten();

Das hat zur Folge, dass lediglich ein getter/setter in Freundin existieren muss und somit Freundin von Änderungen in den Zuständen nicht berührt wird.
Gruß
Karl Heinz
WJ 2011-11-13 19:37:38
Vielen Dank!
Philipp 2011-10-19 10:36:38
Danke für den Hinweis. Ist eingearbeitet.

Seite: 1 - nächste Seite