|
Home - Programmieren - Entwurfsmuster: State
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:
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:
Die Stati im ÜberblickWir 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:
Status-spezifisches VerhaltenDie 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).
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
Der objektorientierte AufbauEs 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
|
(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.
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.
if/else if]-Blöcke und Auswahlanweisungen (switch/case).