Command Pattern
🚧 | Diese Seite befindet sich in Bearbeitung | 🚧 |
Beispielprojekt:
Für diesen Artikel gibt es begleitend ein Beispielprojekt in dem die hier vorgestellte Thematik auch Stufenweise eingeführt wird. Das Projekt selbst ist auch im Detail dokumentiert und kann demnach auch ohne diesen Artikel nachvollzogen werden.
Einleitung:
Wir wissen aus dem Artikel IO/UI schon, dass es Konvention ist eine gewisse Trennung zwischen der Benutzerinteraktion und dem Modell des Programms einzuhalten.
In der Theorie ganz leicht... oder?
Wir schauen uns mal ein simples Modell eines Spiels an:
Das Modell (V1):
Die Klassenstruktur ist wiefolgt:
- Player
- Field
- FieldType
- Game
Es gibt also ein Spielbrett (hier einfach nur eine Menge an Field's statt einem separaten Objekt) mit verschiedenen Kacheln und einem/mehreren Spieler/n. Implementieren wollen wir jetzt folgende Kommandos:
- Move(x, y), bewegt den Spieler auf das Feld mit den Koordinaten X,Y
- ScorePlayer, zeigt die Punkte des aktuellen Spielers an
- Score, zeigt die Punkte aller Spieler an
- Print, zeigt den aktuellen Zustand des Spielfelds an
- Quit, beendet das Spiel
Eine passende Schnittstelle (V2):
Mit einer Schnittstelle ist hier nicht etwa ein Interface gemeint. Wir wollen uns hier ein paar geeignete Methoden definieren, mit denen wir den Zustand des Spiels und alle enthaltenen Objekte einfach und ohne viele Zugriffe verändern können. Dabei versuchen wir jeweils das Spielobjekt in einem Aufruf in den gewünschten Zustand zu überführen. Das heißt unter anderem, dass wir keine Objekte, wie das Spielfeld, zurückgeben und dieses dann verändern, sondern das direkt in einem Aufruf des Spiels erledigen. Das könnte dann wiefolgt aussehen:
Field.java
public void placePlayer(Player player) {
this.player = player;
}
Hier ist die Methode die in einem Feld den Spieler dann tatsächlich speichert.
Game.java
public void placePlayerOnField(int x, int y) throws GameArgumentException {
if (this.board[x][y].getPlayer() != null) {
throw new GameArgumentException("The field is already occupied!");
}
this.board[x][y].placePlayer(players[activePlayerIndex]);
players[activePlayerIndex].increaseScoreBy(board[x][y].getType().getPoints());
}
Diese Methode liegt in der Spielklasse (also der "äußersten" Klasse) und spezifiziert zunächst das Feld, auf das ein Spieler gesetzt werden soll. Das "Wo auf dem Brett?" übernimmt also die Spielklasse und das "Wer?" das eigentliche Feld.
Das ist hier zwar nur ein simples Beispiel, kann aber bei größeren, weiter verschachtelten Programmen genau so angewandt werden.
Commands mit Switch implementieren (V3):
Jetzt wo wir uns schon um eine geeignete Schnittstelle für unser Modell gekümmert haben, müssen wir nur noch dafür sorgen, diese auch richtig zu verwenden. Dafür können wir jetzt in einer Klasse die für die Benutzerinteraktion zuständig sein soll (z.B. GameInstance.java) eine Fallunterscheidung für Eingaben einrichten. (Wie verwenden nicht die Main Klasse da wir ggf. mehrere Instanzen des Spiels erstellen wollen, ohne das gesamte Programm mehrfach zu starten.)
Hierfür verwenden wir erstmal ein "Switch-Statement". Das ist für wenige (5) Kommandos auch schon ausreichend und muss nicht, wie später zu sehen, weiter verschachtelt werden. Wir brauchen also 5 cases für das Switch Statement (für Fehlerbehandlung kann ein default case hilfreich sein). Um die Lesbarkeit beizubehalten erstellen wir für jedes Kommando eine Methode, der wir dann die Argumente des Kommandos übergeben. Innerhalb der Methode wird dann mit der Modellschnittstelle interagiert und Ausgaben getätigt.
Commands bekommen eine eigene Klasse (V4):
In diesem Schritt wenden wir jetzt das Command Pattern an, ein Entwurfsmuster, das später in SWT genauer vorgestellt wird. Für Entwurfsmuster gilt generell, dass es nicht die Lösung gibt.
Was heißt das?
In diesem Fall heißt das insbesondere, dass wir uns aussuchen können, wie genau wir unsere Kommandos abstrahieren möchten. In der Regel werden dafür abstract Classes oder Interfaces verwendet um bestimmte Verhalten vorauszusetzen. Dieses Beispiel wird sich mit abstract Classes beschäftigen, Musterlösungen und andere Ressourcen können und werden andere Methoden verwenden.
Was genau muss ich jetzt tun?
Ziel ist es, Kommandos zu abstrahieren um die Benutzung zu vereinfachen und eine gewisse einfache Erweiterbarkeit herzustellen. Dafür können wir ein abstraktes Kommando definieren, das eine abstrakte execute Methode besitzt (und damit den Kindklassen voraussetzt). Wie sieht dann die Signatur der execute Methode aus?
Hierfür gibt es auch wieder mehrere Möglichkeiten, die je nach persönlicher Präferenz umgesetzt werden können.
Wichtig sind dabei zwei Dinge:
- Das Kommando muss auf das Spiel zugreifen können
- Das Kommando muss verschiedene (und verschieden viele) Argumente entgegen nehmen können
Für Punkt 1 können wir entweder dem Kommando beim Instanziieren die Spielinstanz übergeben (das Kommando kennt also direkt das Spiel) oder mit jedem Kommandoaufruf (also wird die Instanz mit jedem Aufruf der execute Methode neu übergeben). Für Punkt 2 gibt es auch wieder verschiedene Möglichkeiten. Wir können zum Beispiel mit einem Regex ein gewisses Pattern voraussetzen um damit eine vielzahl Fehler schon vorher zu erkennen. Wir können auch den Inputstring aufteilen und die Anzahl der Argumente prüfen. Egal wie, müssen wir entweder einen String oder ein String-Array übergeben. Durch beide Methoden können wir beliebige und beliebig viele Argumente übergeben.
Wichtig:
Innerhalb des Kommandos sollte nur die Syntax der Argumente geprüft werden. Für die Semantik ist das Spiel zuständig.
Was genau heißt das?
Wir prüfen zum Beispiel innerhalb des Kommandos bereits ob eine Zeichenkette in einen Interger überführt werden kann (Erinnerung: Integer.parseInt(String)). Ob die Zahl jetzt in einem move() Kommando Sinn ergibt (weil der Spieler dadurch zum Beispiel außerhalb des Spielfeldes wäre) soll dann das Modell selbst prüfen und das entsprechende Ergebnis zurückgeben.
Statt Switch in der Main ein Command Handler Objekt (V5):
Für mehrere Kommandos eignet sich ein seperates Objekt, dass nicht nur die Kommandos, sondern auch die Ein und Ausgaben verwaltet. Dafür wollen wir jetzt einen Command Handler erstellen. Für diesen wollen wir eine Methode um die Interaktion zu starten. Innerhalb dieser wird dann die Schleife gestartet die Eingaben ließt, Kommandos ausführt und deren Ausgaben tätigt. Um manche Kommandos auszuführen, brauchen die Kommandos selbst eine Referenz zu dem Command Handler. Ein Beispiel dafür ist das Quit Kommando, das einen geeigneten Boolean in der Schleife umstellt um aus dieser auszubrechen und das Program zu beenden. Zusätzlich wollen wir weiterhin einfach prüfen ob wir ein gewisses Kommando kennen oder nicht. Dafür speichern wie die Kommandos bei der Instanziierung des Handlers in einer Hashmap ab, um einfache und schnelle abfragen zu ermöglichen. Um das Program jetzt in der Main Klasse zu starten müssen wir nur eine Spielinstanz erstellen, einem Command Handler übergeben und diesen anschließend mit unserer Methode starten.
Wenn du diese Seite interessant fandest, findest du hier noch mehr Seite(n) dazu:
Parsen, Regex