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("Hallo!");
        } 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, mir gehts super!");
        } 
    }
    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) etwas gesagt hat, was der Freundin missfällt. 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("Hallo!");
    }
    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, mir gehts super!");
    }
}

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("Hallo!");
    }
    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, mir gehts super!");
    }
    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 Zustand) 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 Zustand 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(Zustand aktuellerZustand){
        this.aktuellerZustand = aktuellerZustand;
    }

    //Defaultzustand Neutral im Konstruktor setzen.
    public Freundin(){
        setAktuellerZustand(new Neutral(this));
    }
    
    //Rest wie gehabt.
    private Zustand 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 Zustand{
    public void unterhalten();
    public void kussGeben();
    public void verärgern();
}

class Neutral implements Zustand{
    //Konstruktur. Mit Freundin parametrisiert
    public Neutral(Freundin freundin){
        this.freundin = freundin;
    }
    //Referenz auf die Freundin
    private Freundin freundin;
    
    public void unterhalten() {
        System.out.println("Hallo!");
    }
    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 Zustand{
    //Konstruktur. Mit Freundin parametrisiert
    public Bockig(Freundin freundin){
        this.freundin = freundin;
    }
    //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 Zustand{
    //Konstruktur. Mit Freundin parametrisiert
    public Fröhlich(Freundin freundin){
        this.freundin = freundin;
    }
    //Referenz auf die Freundin
    private Freundin freundin;
 
    public void unterhalten() {
        System.out.println("Hihi, mir gehts super!");
    }
    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(); //Hallo!
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, mir gehts super!			

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 (State). 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 State state;
    public void operate(){
        state.operate();
    }
}			
State-Interface und Implementierungen:

interface State{
    public void operate();
}
class ConcreteState1 implements State{
    public void operate() {
        System.out.println("ConcreteState1");
        //ggf. Zustandswechsel, dazu Handle auf Context mit entsprechenden Setter notwendig
    }
}
class ConcreteState2 implements State{
    public void operate() {
        System.out.println("ConcreteState2");
        //ggf. Zustandswechsel, dazu Handle auf Context mit entsprechenden Setter notwendig
    }
}
class ConcreteState3 implements State{
    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 State currentState;
    public void operate(){
        currentState.operate();
    }
    //Setter zum Setzen des aktuellen Zustands
    public void setState(State state){
        currentState = state;
    }
    
    //Context hält alle möglichen Zustände vor
    private State concreteState1;
    private State concreteState2;
    private State concreteState3;
    
    //Getter für die möglichen Zustände
    public State getConcreteState1() {
        return concreteState1;
    }
    public State getConcreteState2() {
        return concreteState2;
    }
    public State 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 State{
    public void operate();
}
class ConcreteState1 implements State{
    //Zustand muss Context zum Setzen des Folgezustands kennen.
    private final Context context;

    //Konstruktor
    public ConcreteState1(Context context) {
        this.context = context;
    }

    public void operate() {
        System.out.println("ConcreteState1");
        //Zustandswechsel, ggf. mit Bedingung
        context.setState(context.getConcreteState2());
    }
}
class ConcreteState2 implements State{
    //Zustand muss Context zum Setzen des Folgezustands kennen.
    private final Context context;
    
    //Konstruktor
    public ConcreteState2(Context context) {
        this.context = context;
    }
    
    public void operate() {
        System.out.println("ConcreteState2");
        //Zustandswechsel, ggf. mit Bedingung
        context.setState(context.getConcreteState3());
    }
}
class ConcreteState3 implements State{
    //Zustand muss Context zum Setzen des Folgezustands kennen.
    private final Context context;
    
    //Konstruktor
    public ConcreteState3(Context context) {
        this.context = context;
    }
    
    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.