IT-Academy Logo
Sign Up Login Help
Home - Programmieren - Entwurfsmuster: State



Entwurfsmuster: State

Das State-Muster ermöglicht es einem Objekt, sein Verhalten zu ändern, wenn sich dessen interner Zustand verändert. In diesem Artikel wird der Aufbau des State-Musters beschrieben und anschliessend anhand eines Beispiels implementiert.


Autor: Patrick Bucher (paedubucher)
Datum: 27-03-2008, 18:38:19
Referenzen: 'Gang of Four', Entwurfsmuster - Elemente wiederverwendbarer objektorientierter Software, Addison-Wesley 1995
Schwierigkeit: Profis
Ansichten: 6625x
Rating: Bisher keine Bewertung.

Hinweis:

Für den hier dargestellte Inhalt ist nicht der Betreiber der Plattform, sondern der jeweilige Autor verantwortlich.
Falls Sie Missbrauch vermuten, bitten wir Sie, uns unter missbrauch@it-academy.cc zu kontaktieren.

[Druckansicht] [Als E-Mail senden] [Kommentar verfassen]



Gehen wir von einer Applikation aus, die einen Editor enthält. Mit diesem Editor kann man den Inhalt von Dateien bearbeiten. Ein minimalistischer Editor unterstützt in der Regel folgende Operationen:

  • Neu: Es wird eine neuer, leerer Editor geöffnet.
  • Öffnen: Eine bestehende Datei wird geöffnet, ihr Inhalt wird angezeigt.
  • Speichern: Die Datei wird unter dem Namen abgespeichert, unter dem sie bereits zuvor abgespeichert wurde. Hat noch keine Speicherung stattgefunden - ist der Dateiname somit noch nicht bekannt - muss der Benutzer einen Dateinamen angeben.
  • Speichern unter: Die Datei wird unter einem Namen abgespeichert, den der Benutzer angibt. Das Verhalten gleicht sich also dem Verhalten der Operation "Speichern", wenn der Benutzer die Datei noch nie abgespeichert hat.

Wie man bereits sehen kann, unterscheiden sich die Operationen nach einer bestimmten Bedingung; wurde die Datei noch nie abgespeichert, so muss der Benutzer auch bei der Operation "Speichern" einen Dateinamen angeben (analog "Speichern unter"). Das Verhalten soll sich also je nach Status unterscheiden. Bei einem Dateieditor kann man des weiteren zwischen den folgenden beiden Stati unterscheiden:

  • Dirty: Der Editor wurde seit der letzten Speicherung bearbeitet und müsste wieder abgespeichert werden, damit er mit dem Dateisystem synchron ist.
  • Clean: Der Editor wurde seit der letzten Speicherung nicht bearbeitet und ist somit synchron mit dem Dateisystem.

Die Stati im Überblick

Wir unterscheiden also zwischen einem "sauberen" (clean) und einem "dreckigen" (dirty) Editor. Zudem müssen wir bei jeder Operation bedenken, ob der Editor zumindest einmal abgespeichert wurde (und uns somit ein Dateiname zur Verfügung steht). Kombiniert man diese Kriterien, kann man insgesamt vier Stati definieren:

  1. CleanUnsavedState: Der Editor ist unverändert (clean), auch wurde die Datei noch niemals abgespeichert (unsaved).
  2. DirtyUnsavedState: Der Editor ist verändert (dirty) und wurde noch nie abgespeichert (unsaved).
  3. CleanSavedState: Der Editor ist unverändert (clean), die Datei wurde bereits abgespeichert (saved).
  4. DirtySavedState: Der Editor ist verändert (dirty), wurde jedoch bereits einmal abgespeichert (saved).

Status-spezifisches Verhalten

Die vier Operationen ("Neu", "Öffnen", "Speichern" und "Speichern unter") können sich je nach Status unterschiedlich verhalten. Wie sich welche Operation verhalten soll, findet man am besten heraus, wenn man es für jeden Status einzeln definiert. Ein Vorteil am Status-Muster liegt darin, dass die Zustandsübergänge explizit sind und nicht aufgrund einer Summe verschiedenster Kriterien auszumachen sind. Somit ist auch gleich zu definieren, welche Operation von einem Status in einen anderen Status überführen (Transition).

  • CleanUnsavedState
    • Neu: Der Editor wurde weder verändert, noch wurde er abgespeichert - der Benutzer befindet sich schon auf einem neuen Editor. Es bleibt also nichts zu tun (bei einer Applikation mit mehreren Editoren könnte man trotzdem einen neuen Editor erstellen). Neuer Status: CleanUnsavedState (unverändert).
    • Öffnen: Der Benutzer soll eine Datei zum Öffnen auswählen können. Neuer Status: CleanSavedState (dem Editor liegt nun eine Datei zu Grunde).
    • Speichern: Eine Speicherung macht keinen Sinn, da wir gar keinen Inhalt für die Datei haben. Das Programm tut nichts - keine Transition notwendig.
    • Speichern unter: Analog "Speichern"; kein Inhalt - keine Speicherung, keine Transition.
  • DirtyUnsavedState
    • Neu: Da der Benutzer bereits Änderungen am Editor vorgenommen hat, sollten wir ihn fragen, ob er denn die bisherigen Änderungen abspeichern will (erübrigt sich bei einer Applikation mit mehreren Editoren). Anschliessend erhält der Benutzer einen neuen Editor mit dem Status CleanUnsavedState.
    • Öffnen: Analog "Neu", nur dass dem Editor nach dem Öffnen bereits eine Datei zu Grunde liegt - CleanSavedState.
    • Speichern: Der Inhalt des Editors wird als Datei abgespeichert. Da für diesen Inhalt noch nie eine Speicherung stattgefunden hat, müssen wir den Benutzer zur Eingabe eines Dateinamens auffordern. Neuer Status: CleanSavedState.
    • Speichern unter: analog "Speichern".
  • CleanSavedState
    • Neu: Wir können ohne Abfrage (sämtliche Änderungen wurden abgespeichert) einen neuen Editor öffnen. Neuer Zustand: CleanUnsavedState.
    • Öffnen: Beim Öffnen einer Datei muss ebenfalls keine Abfrage stattfinden, da es keine Änderungen gibt. Hier ist strenggenommen keine Transition notwendig, für die Implementierung ist jedoch zu beachten, dass der zugrundeliegende Dateiname geändert hat.
    • Speichern: Da keine Änderungen vorgenommen wurden, muss auch keine Speicherung durchgeführt werden.
    • Speichern unter: Es wurden zwar keine Änderungen vorgenommen, der Benutzer möchte aber möglicherweise eine Kopie der bestehenden Datei anlegen. So fordern wir ihn zur Eingabe eines Dateinamens auf, unter welchem dann der Inhalt des Editors wieder abgespeichert wird. Die Transition ist analog zur Operation "Öffnen".
  • DirtySavedState
    • Neu: Siehe DirtyUnsavedState; es muss eine Abfrage erscheinen, ob der Benutzer seine Änderungen abspeichern möchte. Neuer Zustand: CleanUnsavedState.
    • Öffnen: Analog "Neu", nur dass der Zustand nach dem Öffnen CleanSavedState lautet.
    • Speichern: Die Datei wird unter dem zuletzt verwendeten Dateinamen abgespeichert. Neuer Status: CleanSavedState.
    • Speichern unter: Die Datei wird unter einem Dateinamen abgespeichert, die der Benutzer angibt. Der neue Status lautet hier ebenfalls CleanSavedState.

Neben den Dateioperationen gibt es noch weitere Ereignisse, die eine Transition erfordern. So führen bestimmte Operationen innerhalb des Editors zu einer Zustandsänderung. Da es in diesem Beispiel jedoch um die Abbildung der Dateioperationen als Status geht, ist die Art des Editors mit den damit zugehörigen Editoroperationen zu vernachlässigen. Generell führen Editoroperationen zu einem Dirty-Zustand, Dateioperationen führen zu einem Clean-Zustand. Statusübergänge und objektorientierte Programmierung; dies schreit gerade zu nach einem UML Zustandsdiagramm (die Selbsttransitionen wurden aus Platzgründen weggelassen):


Der objektorientierte Aufbau

Es wird Zeit die gewonnen Erkenntnisse in Klassen und Schnittstellen zu überführen. Dazu am besten gleich ein UML Klassendiagramm mit einem möglichen Lösungsansatz:


Die Schnittstelle Context

Die Schnittstelle Context repräsentiert den Kontext der Anwendung. In unserem Beispiel eines Editors stellt man sich dies am besten als Benutzeroberfläche vor - die Benutzerschnittstelle wird die Schnittstelle Context implementieren. Zu den einzelnen Operationen:

  • setState: Diese Operation erwartet einen neuen Status (Instanz einer Implementierung der Schnittstelle State). Mit dieser Operation kann der Status auf dem Kontext geändert werden. Es handelt sich also hierbei um die Implementierung der Transitionen.
  • newFile: Der Kontext wird dazu aufgefordert, eine neue Datei zu erstellen. Wozu dies sinnvoll ist, sehen wir beim Abschnitt über die Implementierung.
  • openFile: Der Kontext wird dazu aufgefordert, eine Datei zu öffnen (bzw. den Anwender dazu aufzufordern, eine Datei zum Öffnen auszuwählen). Diese Operation soll zugleich den Dateinamen der geöffneten Datei zurückgeben.

Die Schnittstelle State

Bei der Schnittstelle State handelt es sich um die Repräsentation eines Status. Ein Kontext kann jeweils nur einen Status inne haben, dieser wird ihm mit der bereits beschriebenen Operation setState gesetzt. Die Schnittstelle State hat folgenden Operationen:

  • newFile: "Neu", der Benutzer möchte eine neue Datei erstellen.
  • open: "Öffnen", der Benutzer möchte eine bestehende Datei öffnen.
  • save: "Speichern", der Benutzer möchte den Editor unter dem zuletzt verwendeten Dateinamen abspeichern.
  • saveAs: "Speichern unter", der Benutzer möchte den Editor unter einem bestimmten Dateinamen abspeichern.
  • changed: Repräsentiert Operationen auf dem Editor, wie z.B. das Hinzufügen oder Löschen von Inhalt. In diesem Fall wird der Editor "dirty".

Die Implementierung (in Java)

Damit die hier geschilderten Zusammenhänge etwas klarer werden, wird das ganze Beispiel noch in Java implementiert. Die eigentlichen Operationen wie das Speichern und Öffnen von Daten sowie die Implementierung des Editors werden hier ausgelassen, dies ist Aufgabe des jeweiligen Kontext. Ziel der Implementierung ist es in erster Linie, die beschriebenen Transitionen in Quellcode zu giessen.

Die Interfaces State und Context

Die Schnittstellen werden eins zu eins vom Klassendiagramm übernommen:

(01)
(02)
(03)
(04)
(05)
(06)
(07)
(08)
(09)
(10)
(11)
package state;

public interface Context {

  public void setState(State state);
    
  public void newFile();
      
  public String openFile();
            
}

(01)
(02)
(03)
(04)
(05)
(06)
(07)
(08)
(09)
(10)
(11)
(12)
(13)
(14)
(15)
package state;

public interface State {
  
    public void newFile();
      
    public void open();
          
    public void save();
             
    public void saveAs();
                  
    public void changed();

}

CleanUnsavedState

(01)
(02)
(03)
(04)
(05)
(06)
(07)
(08)
(09)
(10)
(11)
(12)
(13)
(14)
(15)
(16)
(17)
(18)
(19)
(20)
(21)
(22)
(23)
(24)
(25)
(26)
(27)
(28)
(29)
(30)
(31)
(32)
package state;

public class CleanUnsavedState implements State {

  private Context context;

  public CleanUnsavedState(Context context) {
    this.context = context;
  }

  public void newFile() {
    // keine Aktion
  }

  public void open() {
    String filename = context.openFile();
    context.setState(new CleanSavedState(context, filename));
  }

  public void save() {
    // keine Aktion
  }

  public void saveAs() {
    // keine Aktion
  }

  public void changed() {
    context.setState(new DirtyUnsavedState(context));
  }

}

Im Konstruktor erwarten wir einen Kontext, den wir als Eigenschaft ablegen. Die Aktionen newFile, save und saveAs erfordern keine Aktionen, da im Editor noch keine Änderungen vorgenommen wurden (es handelt sich hierbei um den Anfangszustand). Die Methode open lässt den Kontext eine Datei öffnen. Es muss der Dateiname zurück gegeben werden, damit wir das Progarmm in einen CleanSavedState überführen können. Erfährt der Editor eine Änderung (Methode changed), so ändern wir den Status nach DirtyUnsavedState.

DirtyUnsavedState

(01)
(02)
(03)
(04)
(05)
(06)
(07)
(08)
(09)
(10)
(11)
(12)
(13)
(14)
(15)
(16)
(17)
(18)
(19)
(20)
(21)
(22)
(23)
(24)
(25)
(26)
(27)
(28)
(29)
(30)
(31)
(32)
(33)
(34)
(35)
(36)
(37)
(38)
package state;

public class DirtyUnsavedState implements State {

  private Context context;

  public DirtyUnsavedState(Context context) {
    this.context = context;
  }

  public void newFile() {
    // Benutzer fragen, ob Änderungen gespeichert werden sollen
    context.setState(new CleanUnsavedState(context));
  }

  public void open() {
    // Benutzer fragen, ob Änderungen gespeichert werden sollen
    String filename = context.openFile();
    context.setState(new CleanSavedState(context, filename));
  }

  public void save() {
    // Es ist noch kein Dateiname vorhanden - das Verhalten ist analog zu saveAs
    saveAs();
  }

  public void saveAs() {
    // speichern unter benutzerdefiniertem Dateinamen
    // filename = neuer Dateiname
    context.setState(new CleanSavedState(context, "[filename]"));
  }

  public void changed() {
    // keine Aktion (dirty bleibt dirty)
  }

}

Der Konstruktor erwartet hier ebenfalls einen Kontext, der wiederum als Eigenschaft abgelegt wird. Die Methode newFile muss dann den Benutzer zuerst fragen, ob er die bereits vorgenommenen Änderungen - wir befinden uns in einem "dirty"-Status - abspeichern möchte. Dies wurde hier nicht implementiert, da es sich eigentlich um eine Editor-spezifische Operation handelt. Danach kann der Kontext zurück in den Status CleanUnsavedState - in den Anfangsstatus - zurückgeführt werden. Die Methode open ist analog zum CleanUnsavedState implementiert, nur dass wir hier den Benutzer fragen müssen, ob er seine Änderungen übernehmen möchte. Bei save delegieren wir nur an die Methode saveAs weiter, da uns noch kein Dateiname zur Verfügung steht. saveAs führt dann die eigentliche Speicherung durch, der Benutzer muss dazu einen Dateinamen angeben. Der Kontext wird sogleich in den Status CleanSavedState überführt, dazu müssen wir den Dateinamen mitgeben. Auf Änderungen (changed) ist nicht zu reagieren, da wir bereits in einem "dirty"-Status sind.

CleanSavedState

(01)
(02)
(03)
(04)
(05)
(06)
(07)
(08)
(09)
(10)
(11)
(12)
(13)
(14)
(15)
(16)
(17)
(18)
(19)
(20)
(21)
(22)
(23)
(24)
(25)
(26)
(27)
(28)
(29)
(30)
(31)
(32)
(33)
(34)
(35)
(36)
(37)
package state;

public class CleanSavedState implements State {

  private Context context;

  private String filename;

  public CleanSavedState(Context context, String filename) {
    this.context = context;
    this.filename = filename;
  }

  public void newFile() {
    context.newFile();
    context.setState(new CleanUnsavedState(context));
  }

  public void open() {
    String filename = context.openFile();
    context.setState(new CleanSavedState(context, filename));
  }

  public void save() {
    // keine Aktion
  }

  public void saveAs() {
    // speichern unter benutzerdefiniertem Dateinamen
    // this.filename = neuer Dateiname
  }

  public void changed() {
    context.setState(new DirtySavedState(context, filename));
  }

}

Stati mit dem Begriff "Saved" benötigen jeweils einen Dateinamen im Konstruktor, sie müssen schliesslich wissen, wohin save-Operationen auszuführen sind. Der Dateiname wird, wie der Kontext, als Eigenschaft abgelegt. Die Operationen newFile und open sind analog dem CleanUnsavedState implementiert. Die Methode save hat in diesem Fall nichts zu tun, da seit der letzten Speicherung keine Änderungen vorgenommen wurden, saveAs führt - wie üblich - eine Speicherung auf einen benutzerdefinierten Dateinamen aus. Hierbei ist die Eigenschaft filename mit dem neuen Dateinamen zu überschreiben, da der Benutzer nun auf einer anderen Datei arbeitet. In diesem Status wird die Eigenschaft filename eigentlich gar nicht benötigt, um die Datei abzuspeichern. Es geht vielmehr darum, den Dateinamen bereit zu halten für den Fall, dass die Methode changed aufgerufen wird und wir zum Status DirtySavedState übergehen müssen. Dieser Status benötigt dann wiederum einen Dateinamen.

DirtySavedState

(01)
(02)
(03)
(04)
(05)
(06)
(07)
(08)
(09)
(10)
(11)
(12)
(13)
(14)
(15)
(16)
(17)
(18)
(19)
(20)
(21)
(22)
(23)
(24)
(25)
(26)
(27)
(28)
(29)
(30)
(31)
(32)
(33)
(34)
(35)
(36)
(37)
(38)
(39)
(40)
package state;

public class DirtySavedState implements State {

  private Context context;

  private String filename;

  public DirtySavedState(Context context, String filename) {
    this.context = context;
    this.filename = filename;
  }

  public void newFile() {
    // Benutzer fragen, ob Änderungen gespeichert werden sollen
    context.setState(new CleanUnsavedState(context));
  }

  public void open() {
    // Benutzer fragen, ob Änderungen gespeichert werden sollen
    String filename = context.openFile();
    context.setState(new CleanSavedState(context, filename));
  }

  public void save() {
    // speichern unter 'filename'
    context.setState(new CleanSavedState(context, filename));
  }

  public void saveAs() {
    // speichern unter benutzerdefiniertem Dateinamen
    // filename = neuer Dateiname
    context.setState(new CleanSavedState(context, "[filename]"));
  }

  public void changed() {
    // keine Aktion (dirty bleibt dirty)
  }

}

Der Konstruktor des Status DirtySavedState benötigt neben dem Kontext auch einen Dateinamen. Beide Angaben werden als Eigenschaften abgelegt. Die Operationen newFile und open sind analog zum Status DirtyUnsavedState implementiert. Die Methode save speichert die Datei unter dem Dateinamen ab, der dem Konstruktor übergeben wurde, es sei denn, dieser wurde durch saveAs überschrieben. Beide Speicher-Operationen führen zum Status CleanSavedState. Die Methode changed hat nichts zu tun, da wir uns bereits in einem "dirty"-Zustand befinden.

Mögliche Abweichungen

Eine weitere Möglichkeit wäre es, statt einem Interface State eine abstrakte Klasse zu definieren. Diese könnte dann z.B. die Methode changed als leere Methode, d.h. ohne jegliche Operation, bereitstellen. In diese Falle müsste changed nur noch durch die beiden "clean"-States überschrieben werden. Angesichts dieses kleinen Unterschieds bleibt es Geschmackssache, ob man sich nun für ein Interface oder gleich eine abstrakte Klasse entscheidet, müssen doch die anderen Operationen in der Regel für jeden Status einzeln implementiert werden.

Weiter könnte die Implementierung der State-Klassen als Singleton erfolgen. In diesem Falle müssten während der Programmlaufzeit weniger Objekte erstellt und vernichtet werden, dies könnte jedoch zu Problemen im Zusammenhang mit Referenzen von den Status-Objekten aus führen. Ein Beispiel dafür wären mehrere Kontexte, die während der Laufzeit auf- und abgebaut würden.
In diesem Artikel wurde auf die Implementierung als Singleton schlichtweg aus Gründen der Einfachheit verzichtet; hier geht es um das State- und nicht um das Singleton-Muster (siehe Artikel zum Singleton-Muster).

Es stellt sich auch die Frage, wo denn Transitionen zu implementieren sind. In diesem Beispiel übernehmen die Stati die Zustandsübergänge selbst. Alternativ könnte sich auch der Kontext darum kümmern. Vom Aspekt der Kapselung her betrachtet ist aber die hier verwendete Lösung die bessere; der Kontext braucht nur zu wissen, dass er einen Zustand hat und wie er ihn verwenden kann. Ob und wie sich der Zustand dann ändert, braucht den Kontext nicht zu kümmern.

Konsequenzen

  • Die Zustände werden explizit (als Klassen) implementiert.
    • Zustandsänderungen sind immer explizit, der Kontext befindet sich immer in einem bestimmten Zustand.
    • Zustand-spezifisches Verhalten wird gekapselt, dies fördert die Verständlichkeit des Programmcodes und schafft einen besseren Überblick.
    • Man kann das Programm durch die Definition einer neuen Zustandsklasse leicht um weitere Zustände erweitern.
  • Man erspart sich grosse [if/else if]-Blöcke und Auswahlanweisungen (switch/case).
    • Der Code ist einfacher zu lesen und vor allem weniger fehleranfällig.
    • Dadurch ist der Code auch besser wartbar.
  • Es werden mehr Schnittstellen und Klassen benötigt.
    • Eine Menge kleiner Klassen wird erstellt.
    • Es werden zur Laufzeit mehr Objekte erstellt und entsorgt (dies kann durch das Singleton-Muster jedoch etwas entschärft werden), womöglich wird auch mehr Speicher benötigt.
    • Können mehrere Kontexte die gleichen Status-Objekte verwenden (sofern die Stati durch ihre jeweiligen Klassen und nicht durch Instanzen definierbar sind), könnte Speicherplatz eingespart werden.


[back to top]



Userdaten
User nicht eingeloggt

Gesamtranking
Werbung
Datenbankstand
Autoren:04505
Artikel:00815
Glossar:04116
News:13565
Userbeiträge:16552
Queueeinträge:06241
News Umfrage
Ihre Anforderungen an ein Online-Zeiterfassungs-Produkt?
Mobile Nutzung möglich (Ipone, Android)
Externe API Schnittstelle/Plugins dritter
Zeiterfassung meiner Mitarbeiter
Exportieren in CSV/XLS
Siehe Kommentar



[Results] | [Archiv] Votes: 1140
Comments: 0