Entwurfsmuster (Design Patterns): Unterschied zwischen den Versionen
(→Die View - einfachste Version) |
(→Der oder die Controller) |
||
Zeile 895: | Zeile 895: | ||
Um das Spiel wirklich spielen zu können, fehlt aber noch etwas... | Um das Spiel wirklich spielen zu können, fehlt aber noch etwas... | ||
− | + | ===Der oder die Controller=== | |
Im MVC-Muster soll der Controller als der "Vermittler" fungieren, der Benutzereingaben von der View an das Modell weiterreicht. Es gibt viele verschiedene Beschreibungen der genauen Arbeitsweise des Controllers. Teilweise wird der Controller auch als eine Art Relikt angesehen, das in den ersten Implementierungen des MVC-Musters noch eine klar definierte Funktion hatte, heutzutage aber nicht mehr als eigenständiges Modul angesehen werden kann oder muss. | Im MVC-Muster soll der Controller als der "Vermittler" fungieren, der Benutzereingaben von der View an das Modell weiterreicht. Es gibt viele verschiedene Beschreibungen der genauen Arbeitsweise des Controllers. Teilweise wird der Controller auch als eine Art Relikt angesehen, das in den ersten Implementierungen des MVC-Musters noch eine klar definierte Funktion hatte, heutzutage aber nicht mehr als eigenständiges Modul angesehen werden kann oder muss. | ||
Zeile 905: | Zeile 905: | ||
*Der Controller ist ein Listener, der Eingaben aus der View an das Modell weiterreicht | *Der Controller ist ein Listener, der Eingaben aus der View an das Modell weiterreicht | ||
*Der Controller ist ein Teil der View - zum Beispiel eine innere Klasse in der View | *Der Controller ist ein Teil der View - zum Beispiel eine innere Klasse in der View | ||
− | |||
====Ein einfacher Controller==== | ====Ein einfacher Controller==== |
Version vom 10. August 2013, 17:38 Uhr
Beim Programmieren stellt man fest, dass bestimmte Schemata sich oft wiederholen (meist mit geringen Unterschieden) - diese Schemata bzw. Muster nennt man Entwurfsmuster (Design Patterns).
Entwurfsmuster beschreiben die Kommunikation von Objekten in einer Art die einem eine flexibel und leicht erweiterbare Software Architektur gewährleistet. Sie helfen einem beim Entwickeln bzw. Entwerfen eines gültigen Systems.
Viele Entwurfsmuster wurden über die Jahre dokumentiert und sollen hier nun vorgestellt werden. Hier werden die wichtigsten Entwurfsmuster vorgestellt, wie sie zu verwenden sind und warum man sie nutzen sollte.
Hierbei möchte ich doch darauf hinweisen, dass diese Auflistung nur eine kleine Auswahl sein kann und auch nicht auf jedes Pattern in Tiefe eingegangen wird (z.B. Sinn & Unsinn eines Patterns) - dafür sollte dann die Literatur zu Rate gezogen werden.
Es werden auch keine J2EE Patterns vorgestellt... über die gibt es hier eine gute Übersicht.
Inhaltsverzeichnis
- 1 Singleton
- 2 Observer
- 3 Iterator
- 4 Fasade
- 5 General Hierarchie
- 6 Player Role Pattern
- 7 Immutable
- 8 Read-Only
- 9 Factory
- 10 Das MVC (Architekturmuster)
- 11 Visitor
Singleton
Das Singleton Pattern stellt sicher, dass es von einer Klasse nur eine Instanz gibt.
Oft verwendete Beispiele sind Datenbankmanager Klassen (um nicht in den Konflikt zu kommen mehrer DB Connections handeln zu müssen o.ä.) oder eine Configurations Klasse, die für mehrere Klassen in der Anwendung wichtige Informationen bereit stellt.
Um ein Singleton zu erstellen, gibt es zwei Möglichkeiten: <code=java>public class Singleton {
public static final Singleton instance = new Singleton();
private Singleton() { }
}</code=java>
<code=java>public class Singleton {
private static final Singleton instance = new Singleton(); private Singleton() { }
public static Singleton getInstance() { return instance; }
}
</code=java>
In beiden Fällen ist der private Konstruktor wichtig, der verhindert, dass die Klasse von außerhalb instanziiert werden kann. Beide Versionen unterscheiden nur in der static final
Variable. Deklariert man sie als private
, muss eine public
Methode gegegeben sein, um die Instanz zu erhalten. Ansonsten sind beide Versionen equivalent.
Der Vorteil der zweiten Version ist aber, dass man sie leicht abändern kann, wenn man z.b. nicht nur eine, sondern für jeden vorhandenen Thread eine eindeutige Instanz erzeugen will.
Oft sieht man auch folgende Variante: <code=java>public class Singleton {
private static Singleton instance; private Singleton() { }
public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }
}</code=java>
Wichtig hierbei ist die Methode synchronized
zu definieren, so dass die Methode Thread sicher ist!
Eine Möglichkeit wäre, die Verwendung einer final
Klasse mit statischen Methoden (wie Math Klasse), diese Art behindert aber das Umschalten zwischen Singleton Klasse und "normaler" Klasse.
Anmerkung von Bleiglanz:
Die 1. und 2. Lösung sind für fast alle einfachen Fälle - das ist fast immer am besten (wobei die Variante mit getInstance zu bevorzugen ist).
die synchronized Lösung NUR DANN, wenn "lazy" Sinn macht [weil der Konstruktor "zu lange braucht"]
Auf das sog. Double Checked Locking sollte komplett verzichtet werden - siehe Double Checked Locking Is Broken
Observer
Das Observer Pattern hilft bei der Kommunikation von Objekten, ohne dass Instanzen voneinander bekannt sind.
Vor allem in der GUI Programmierung hat man meist das Problem, dass Daten, die in einer GUI angezeigt werden sich im Laufe des Programms ändern. Diese Änderungen sollen dann in der GUI sichtbar gemacht werden.
Die häufigste Lösung dieses Problems ist es eine doppelte Assoziation der beiden Klassen zu verwenden, dh. die GUI kennt direkt die Datenklasse und die Datenklasse kennt die GUI...
Das löst zwar das Problem der Kommunikation, bringt aber andere einige Probleme mit sich. Durch die Assoziation macht man die beiden Klassen von einander abhängig, d.h. die eine Klasse kann nur arbeiten bzw. kompilieren, wenn die andere Klasse vorhanden ist. Bzw. wenn man später z.B. die GUIKlasse komplett ändern will, muss man immer auch die Datenklasse ändern, oder wenn man die Änderungen nicht nur in einer GUI anzeigen, sondern auch in einer anderen Klasse sichtbar machen will (z.b. in eine Datenbank schreiben), hat man hier unzählige Änderungen und aufgeblähten Code...
Man spricht hier dann von einem Verstoß gegen das MVC Prinzip. MVC steht für Model - View - Controller und bedeutet, dass man die drei Ebenen einer Anwendung (die Datenebene = Model / die GUI = View / die kontrollierende Ebene = Controller) nicht mit einander mischen darf.
Das Observer Pattern ist nun eine gute Möglichkeit, die Kommunikation zwischen den Ebenen zu gewährleisten ohne gegen das MVC Prinzip zu verstoßen:
Man erstellt sich eine abstrakte Klasse Observable, die eine Reihe von Observer hält. Diese Klasse braucht nur Methoden um Observer hinzuzufügen bzw. zu entfernen und ihren Observer Nachrichten zu schicken. Die Klasse Observer wiederum ist ein Interface, das nur die Methode update()
definiert, die dann aufgerufen wird, wenn der Observerable Nachrichten an die Observer schickt. Jede Klasse die "observiert" werden will, implementiert das Observer Interface und meldet sich bei einem Observable an.
Beispiel: <code=java>import java.util.Observable; import java.util.Observer;
import javax.swing.JFrame;
/**
* Die Klasse WetterAnzeige ist die GUI um die Wetterdaten * anzuzeigen. Wenn sich die Daten ändern soll sich auch die GUI ändern. Der * Vorteil des Observer Patterns ist, dass die Klassen kommunizieren können ohne * sich gegenseitig zu kennen ! */
public class WetterAnzeige extends JFrame implements Observer {
public WetterAnzeige() { }
/** * Methode update vom Interface Observer wird * aufgerufen wenn ein Observer notifyObserver * aufruft. Der Observer kann sich bei mehreen * Observable anmelden und man kann der Action ein beliebiges * Object mitgeben. */ public void update(Observable o, Object update) { // im Object update kann z.b. die ßnderungen sein - oder ein spezielles // Event das informationen zu den ßnderungen speichert }
}
class WetterDaten extends Observable {
public void esPassiertWasMitDenDaten() { // es passiert was - Daten ändern sich // der Zustand wird als geänder markiert setChanged();
//alle Observer werden benachrichtigt notifyObservers("Das Wetter wird schön"); }
}
class WetterController {
public static void main(String[] args) { WetterAnzeige view = new WetterAnzeige(); WetterDaten daten = new WetterDaten(); daten.addObserver(view); }
}</code=java> Java stellt das Pattern direkt über die Klassen Observer und Observable zur Verfügung (z.B. beruht die gesamte Listener Struktur in Java auf diesem Pattern). Ein häufiger Fehler ist, das Pattern zwar zu verwenden, aber dennoch den Observer direkt bei dem konkreten Observable anzumelden! Die Anmeldung sollte daher über eine dritte Ebene - der Controller Ebene geschehen!
Eine Anmeldung der Ebenen muss nicht in der Controller Ebene stattfinden, wenn man als Parameter Interfaces benutzt.
D.h. folgendes wäre falsch:
<code=java>class WetterDaten extends Observable {
public WetterDaten(WetterAnzeige zeige) { addObserver(zeige); }
public void esPassiertWasMitDenDaten() { // es passiert was - Daten ändern sich // der Zustand wird als geänder markiert setChanged();
//alle Observer werden benachrichtigt notifyObservers("Das Wetter wird schön"); }
}</code=java>
Hingegen wäre richtig: <code=java>class WetterDaten extends Observable {
public WetterDaten(Observer zeige) { addObserver(zeige); }
public void esPassiertWasMitDenDaten() { // es passiert was - Daten ändern sich // der Zustand wird als geänder markiert setChanged();
//alle Observer werden benachrichtigt notifyObservers("Das Wetter wird schön"); }
}</code=java>
Iterator
Fasade
General Hierarchie
Player Role Pattern
Immutable
Read-Only
Factory
Das MVC (Architekturmuster)
Das MVC Design (Model - View - Controller) beschreibt den Ansatz, den Datenzugriff und die Anwendungslogik von der graphischen Darstellungsart zu trennen. Genauer gesagt können wir MVC in drei Elemente aufteilen:
Model: Das Model enthält die Daten, sowie die Regeln zum Datenzugriff und deren Aktualisierung. Es befinden sich keinerlei Informationen in den Klassen, wie ihre Visualisierung aussieht.
View: Die View stellt den Inhalt eines Models auf der graphischen Benutzeroberfläche dar. Es definiert genau, wie die Modeldaten visualisiert werden sollen. Wenn sich die Modeldaten ändern, muss die View diese Darstellung entsprechend aktualisieren. Dies wird meist dadurch erreicht, dass die View sich beim Model registriert, um über Änderungen informiert zu werden. In der View befinden sich keine Informationen über die Speicherung bzw. Manipulation der Daten.
Controller: Der Controller übersetzt die Benutzeraktionen in Aktionen, die das Model ausführen muss. Eine Benutzeraktion kann z.B. ein Buttonklick oder eine Menüauswahl sein.
Einfaches Beispiel
Folgendes (äußerst simple) Beispiel soll dies demonstrieren:
Die Klasse Wind ist die ModelKlasse. In ihr werden Windrichtung und Windgeschwindigkeit gespeichert. Die Klasse WindViewer ist die ViewKlasse. Sie dient dazu die Daten der Wind Klasse anzuzeigen und man kann über die Buttons die Daten ändern. Die Klasse WindController ist die Controller Klasse. Sie übersetzt die Buttonaktionen in Modelaktionen.
<code=java>package test.model;
/**
* Model * @author deathbyaclown/André Uhres */
import java.util.*;
public class Wind extends Observable {
private Direction dir = Direction.NORTH; private int speed = 0;
/** * @return Returns the dir. */ public Direction getDir() { return dir; }
/** * @param dir The dir to set. */ public void setDir(Direction dir) { this.dir = dir; setChanged(); notifyObservers(this); }
/** * @return Returns the speed. */ public int getSpeed() { return speed; }
/** * @param speed The speed to set. */ public void setSpeed(int speed) { this.speed = speed; setChanged(); notifyObservers(this); }
}</code=java>
<code=java>package test.model;
/**
* @author deathbyaclown */
public enum Direction {
NORTH, EAST, SOUTH, WEST
}</code=java>
<code=java>package test.view;
/**
* View * @author bygones/André Uhres */
import java.awt.*; import java.awt.event.*; import java.util.*; import javax.swing.*; import test.controll.*; import test.model.*;
public class WindViewer extends JFrame implements Observer {
private WindController controller; private JLabel direction; private JLabel speed; private JPanel buttonPanel; private JPanel mainPanel;
public WindViewer(WindController controller) { super("WindViewer"); this.controller = controller; setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); init(); pack(); setLocationRelativeTo(null); setVisible(true); }
private void init() { buttonPanel = new JPanel(); mainPanel = new JPanel(); direction = new JLabel(); speed = new JLabel(); mainPanel.add(new JLabel("Direction: ")); mainPanel.add(direction); mainPanel.add(new JLabel("Speed: ")); mainPanel.add(speed); getContentPane().add(mainPanel);
JButton button = new JButton("Change Direction"); button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) { controller.changeDirection(); } }); buttonPanel.add(button);
button = new JButton("Change Speed"); button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) { controller.changeSpeed(); } }); buttonPanel.add(button); getContentPane().add(buttonPanel, BorderLayout.SOUTH); }
/* * (non-Javadoc) * * @see java.util.Observer#update(java.util.Observable, java.lang.Object) */ public void update(Observable arg0, Object arg1) { Wind wind = (Wind) arg1; direction.setText(wind.getDir().toString()); speed.setText(String.valueOf(wind.getSpeed())); }
}</code=java>
<code=java>package test.controll;
/**
* Controller * @author deathbyaclown/André Uhres */
import javax.swing.*; import test.model.*; import test.view.*;
public class WindController {
private Wind wind;
public WindController() { WindViewer viewer = new WindViewer(this); wind = new Wind(); wind.addObserver(viewer); }
public void changeDirection() { Direction[] dir = Direction.values(); wind.setDir(dir[(int) (Math.random() * 4)]); }
public void changeSpeed() { wind.setSpeed((int) (Math.random() * 100)); }
public static void main(final String[] args) { Runnable gui = new Runnable() {
@Override public void run() { new WindController(); } }; //GUI must start on EventDispatchThread: SwingUtilities.invokeLater(gui); }
}</code=java>
Beispiel von TicTacToe
Kleines Model-View-Controller-Tutorial am Beispiel von TicTacToe
Einleitung
In diesem Tutorial wird beschrieben, wie man eine Anwendung nach dem Model-View-Controller (MVC) - Muster aufbauen kann. Das Tutorial richtet sich an Einsteiger, und soll eine erste Vorstellung von diesem Muster vermitteln. Als Beispiel wird dabei das berühmte Spiel "TicTacToe" verwendet.
WICHTIG: Ziel dieses Tutorials ist nicht, eine besonders gute oder geschickte Implementierung von TicTacToe zu beschreiben, sondern nur zu zeigen, wie man eine Anwendung nach dem MVC-Muster aufbauen kann.
Im Internet findet man viele weiter führende Informationen über das MVC-Muster. Man findet unterschiedliche Auslegungen, Ausprägungen und Abwandlungen des Musters, und viele davon sind auf spezielle Awendungsbereiche zugeschnitten. In Foren wird teilweise heftig diskutiert, wie denn einzelne Aspekte des Musters umzusetzen sind. In diesem Tutorial werden darum grundlegende Ideen an einem konkreten Beispiel verdeutlicht, ohne den Anspruch, alle Sonderfälle abzudecken.
Grundprinzip von MVC
Beim MVC-Muster teilt man seine Anwendung in drei Module auf:
- Model - Das Datenmodell. Es beschreibt das "Ding", das man modellieren will. Es kann ein Modell eines realen Objektes sein, oder eine sinnvolle Zusammenfassung abstrakter Daten.
- View - Die Ansicht oder Präsentation. Sie wird verwendet, um Informationen über das Modell in irgendiner Form auf dem Bildschrim anzuzeigen.
- Controller - Der "Vermittler". Er soll hauptsächlich Benutzereingaben von der View an das Modell weiterreichen.
Das Ziel bei dieser Aufteilung ist es, die zu verarbeitenden Daten (das Modell) von ihrer Präsentation auf dem Bildschirm (der View) zu trennen, und die Vermittlung zwischen diesen Teilen einem Kontrollmodul (dem Controller) zu überlassen. Damit soll erreicht werden, dass die einzelnen Module wiederverwandbar und flexibel einsetzbar sind. Man kann damit erreichen, dass ein Modell mit unterschiedlichen Views dargestellt werden kann, oder dass eine View verschiedene Implementierungen eines Modelles anzeigen kann, ohne dass man am jeweils anderen Programmteil etwas ändern muss. Was genau das bedeutet, wird später in diesem Tutorial noch deutlicher.
TicTacToe nach dem MVC-Muster
Hier wird beschrieben, wie man ein Spiel wie TicTacToe nach dem MVC-Muster aufbauen kann. Es werden mögliche Entwürfe und Umsetzungen für das Modell, die View und den Controller beschrieben, und wie man diese Teile zu einem Programm zusammenfügen kann.
Das Modell
Der Entwurf des Modells ist der Punkt, wo man sich genau klarzumachen muss, was man eigentlich beschreiben will. Im ersten Moment könnte man glauben, dass das einfach sein müßte. Aber es gibt selbst bei scheinbar sehr einfachen Modellen Dinge, die bei genauer Betrachtung schwierig zu entscheiden sind.
Das Modell kann man am besten in einem interface
beschreiben. Die konkrete Implementierung spielt beim Entwurf häufig noch keine so große Rolle. (Ein Ziel des MVC-Modells ist ja gerade, dass man unterschiedliche Implementierung eines Modells verwenden kann). Das interface ist dabei ein "Vertrag" oder eine Vereinbarung, in der verbindlich beschrieben und zusammengefasst wird, was man mit dem Modell machen kann.
Das Spiel TicTacToe könnte man vereinfacht in Worten etwa so beschreiben:
- Es gibt das Spiel bzw. Spielbrett mit 3x3 Feldern
- Es gibt zwei Spieler die durch die Zeichen "X" und "O" beschrieben werden.
- Die Spieler können sehen, auf welchen Feldern schon Spielsteine liegen.
- Die Spieler können ihre Spielsteine auf freie Felder setzen.
- Man kann erkennen, ob das Spiel schon zuende ist, und welcher Spieler gewonnen hat.
- Man kann das Spiel neu starten (indem man alle Spielsteine entfernt)
Diese Beschreibung kann man dann direkt in ein Java-Interface übersetzen, das das Modell beschreibt:
<code=java>
public interface TicTacToeModel
{
// Konstanten für die Spieler. final int PLAYER_NONE = 0; final int PLAYER_X = 1; final int PLAYER_O = 2;
// Liefert zurück, welcher Spieler an der angegebenen // Position einen Spielstein liegen hat int getPiece(int row, int column);
// Setzt einen Spielstein des angegebenen Spielers // an die angegebene Position void setPiece(int row, int column, int player);
// Gibt an, ob das Spiel zuende ist boolean isGameOver();
// Liefert zurück, wer gewonnen hat: // - Wenn das Spiel noch nicht zuende ist, wird // PLAYER_NONE zurückgegeben. // - Wenn ein Spieler gewonnen hat, wird entweder // PLAYER_X oder PLAYER_O zurückgegeben. // - Wenn das Spiel schon zuende ist, aber unentschieden // ausgegangen ist, wird PLAYER_NONE zurückgegeben. int getWinner();
// Startet das Spiel neu void restart();
} </code=java>
Wichtig: Das ist eine stark vereinfachte Darstellung. In einer realen Anwendung müsste man das Modell viel präziser beschreiben. Man müsste festlegen, dass das Spielfeld 3x3 Felder hat. Im Java-Interface würde das dann z.B. verdeutlicht, indem man einen Methodenkommentar einfügt, der die jeweilige Methode genau beschreibt: <code=java>
/** * Returns the constant for the player who has a piece * at the specified field. This may be PLAYER_NONE, * PLAYER_X or PLAYER_O * * @param row The row of the field: May be 0, 1 or 2 * @param column The column of the field: May be 0, 1 or 2 * @return The constant of the player who has a piece * at the specified field * @throws ArrayIndexOutOfBoundsException If the row * or the column is smaller than 0 or greater than 2. */ int getPiece(int row, int column);
</code=java> Zusätzlich müßte man noch weitere Bedingungen beschreiben, die bei Steuerung des Modells eingehalten werden müssen:
- Man darf keinen Spielstein auf eine Position setzen, an dem schon ein Spielstein liegt
- Man darf keine einzelnen Spielsteine wieder wegnehmen
- Man darf keine Spielsteine setzen, wenn das Spiel schon zuende ist
- Die Spieler müssen ihre Spielsteine abwechseld setzen
Darüber hinaus könnte man sich viele alternative Beschreibungen oder Erweiterungen vorstellen. Bei komplexeren Spielen würde man vor allem zwischen dem eigentlichen "Spiel" und dem "Spielbrett" unterscheiden, und diese beiden Teile wiederum nach dem MVC-Muster aufbauen. Zusätzlich könnte man eigene Klassen für die Spieler und Spielfiguren einführen. Das alles würde aber den Rahmen dieses Tutorials sprengen. Die beschriebene Modellierung ist bewußt einfach gehalten, um sich hier auf das Wesentliche konzentrieren zu können.
Das Modell im MVC-Muster wird beobachtet
Das oben beschriebene Modell enthält nun alle Methoden, die man braucht, um das Modell steuern zu können, und abzufragen, in welchem Zustand das Modell gerade ist. Das würde schon reichen, um einen festgelegten Spielablauf durchzuspielen. Allerdings bekommt man keine Information darüber, wenn sich am Modell etwas ändert. Wenn etwa ein Spieler einen Spielstein setzt, wird der andere Spieler darüber nicht informiert.
Um dieses Problem zu lösen, muss man das Modell im MVC-Muster beobachten können. Dieser Teil des MVC-Musters ist ein eigenes Entwurfsmuster, nämlich das Beobachter (Observer)-Entwurfsmuster. In der Form, wie das Muster im Rahmen des MVC angewendet wird, kann man es so zusammenfassen:
[*]Das Modell ist ein beobachtbares Objekt (Observable), das alle seine Beobachter über Änderungen informiert [*]Es gibt Beobachter (Listener, Observer) die informiert werden, wenn sich am Modell etwas verändert. Die Beobachter sind als ein Interface beschrieben. [*]Es gibt meistens noch eine "Event"-Klasse, die eine Änderung an einem Modell genauer beschreibt. Bei einer Änderung werden den Listenern dan Event-Objekte übergeben, um sie über die Änderung des Modells zu informieren.
Um das Beobachter-Muster für das Spiel TicTacToe anzuwenden, werden im Modell-Interface nun Methoden angeboten, die es erlauben, Beobachter hinzuzufügen oder zu entfernen:
<code=java>
public interface TicTacToeModel
{
...
void addTicTacToeListener(TicTacToeListener listener); void removeTicTacToeListener(TicTacToeListener listener);
} </code=java>
Es gibt außerdem ein Interface für alle "Beobachter"-Klassen, die über eine Änderung des Spiels informiert werden wollen. Dieses Interface enthält Methoden, die später vom Modell aufgerufen werden, wenn sich im Modell etwas ändert: <code=java> interface TicTacToeListener {
// Diese Methode wird aufgerufen, wenn im Modell ein // neuer Spielstein gesetzt wurde void pieceWasSet(TicTacToeEvent event);
// Diese Methode wird aufgerufen, wenn sich der // Status des Modells geändert hat (also wenn // das Spiel gestartet oder beendet wurde) void statusChanged(TicTacToeEvent event);
} </code=java>
Die Objekte, die an diese Methoden übergeben werden, sind von einer eigenen "Event"-Klasse, die die Änderungen genauer beschreibt: <code=java> class TicTacToeEvent {
// Die Position des gesetzten Spielsteins, und der // Spieler, der den Stein gesetzt hat private int row; private int column; private int player;
// Besagt, ob das spiel zuende ist private boolean gameOver;
// Erstellt einen Event, der beschreibt, dass // ein Spielstein gesetzt wurde public TicTacToeEvent(int row, int column, int player) { this.row = row; this.column = column; this.player = player; } // Erstellt einen Event, der beschreibt, dass // das Spiel beendet oder gestartet wurde public TicTacToeEvent(boolean gameOver) { this.gameOver = gameOver; } // Get-Methoden für alle Eigenschaften: public int getRow() { return row; } public int getColumn() { return column; } public int getPlayer() { return player; }
public boolean isGameOver(){ return gameOver; } // (Es gibt KEINE set-Methoden: Ein Event kann nach // seiner Erstellung nicht mehr verändert werden!)
} </code=java>
Implementierung eines Modells
Bisher wurden folgende Klassen und interfaces beschrieben:
- Ein Interface TicTacToeModel, das beschreibt, wie das Spiel gesteuert werden kann.
- Ein Interface TicTacToeListener, das Methoden enthält, die vom Modell aufgerufen werden sollen, wenn es verändert wird
- Eine Klasse TicTacToeEvent, die eine Änderung am TicTacToe-Spiel genauer beschreibt.
Diese Klassen können jetzt zu einer ersten Implementierung des TicTacToe-Modells zusammengefasst werden. Die folgende Implementierung ist möglichst einfach gehalten, und die wichtigsten Teile sind kurz kommentiert:
<code=java>
import java.util.*;
// Eine einfache Implementierung des TicTacToeModel-Interfaces public class DefaultTicTacToeModel implements TicTacToeModel {
// Dieser Array speichert das Spielfeld private int board[][] = new int[3][3]; // Gibt an, ob das Spiel zuende ist private boolean gameOver = false;
// Der aktuelle Gewinner private int winner = TicTacToeModel.PLAYER_NONE; // Die Listener, die über Änderungen am Modell // benachrichtigt werden sollen. private List<TicTacToeListener> listeners = new ArrayList<TicTacToeListener>();
// Implementierung des TicTacToeModel-Interfaces: // Liefert zurück, welcher Spieler an der angegebenen // Position einen Spielstein liegen hat @Override public int getPiece(int row, int column) { return board[row][column]; }
// Implementierung des TicTacToeModel-Interfaces: // Setzt einen Spielstein des angegebenen Spielers // an die angegebene Position @Override public void setPiece(int row, int column, int player) { // Nur wenn sich durch das Setzen des neuen // Spielsteins wirklich etwas ändert, muss // überhaupt etwas gemacht werden! if (board[row][column] != player) { board[row][column] = player; // Benachrichtige alle Listener, dass // ein neuer Spielstein gesetzt wurde for (TicTacToeListener listener : listeners) { listener.pieceWasSet( new TicTacToeEvent(row, column, player)); }
// Überprüfe, ob jemand gewonnen hat, und // aktualisiere den Spielstatus updateGameStatus(); } }
// Implementierung des TicTacToeModel-Interfaces: // Gibt an, ob das Spiel zuende ist @Override public boolean isGameOver() { return gameOver; } // Implementierung des TicTacToeModel-Interfaces: // Liefert zurück, wer gewonnen hat @Override public int getWinner() { return winner; } // Implementierung des TicTacToeModel-Interfaces: // Startet das Spiel neu @Override public void restart() { // Wenn das Spiel im Moment läuft, beende es if (!gameOver) { setGameOver(true); } // Leere das Spielfeld for (int row=0; row<3; row++) { for (int column=0; column<3; column++) { board[row][column] = TicTacToeModel.PLAYER_NONE; } } // Setze den Gewinner und den Spielstatus zurück winner = TicTacToeModel.PLAYER_NONE; setGameOver(false); } // Implementierung des TicTacToeModel-Interfaces: // Methoden zum Hinzufügen und Entfernen von Listenern @Override public void addTicTacToeListener(TicTacToeListener listener) { listeners.add(listener); }
// Implementierung des TicTacToeModel-Interfaces: // Methoden zum Hinzufügen und Entfernen von Listenern @Override public void removeTicTacToeListener(TicTacToeListener listener) { listeners.remove(listener); }
// Setzt den neuen Spielstatus: Ob das Spiel zuende // ist oder nicht private void setGameOver(boolean gameNowOver) { // Nur wenn sich der Status ändert, muss // überhaupt etwas gemacht werden if (gameOver != gameNowOver) { gameOver = gameNowOver; // Benachrichtige alle Listener über den // neuen Status for (TicTacToeListener listener : listeners) { listener.statusChanged(new TicTacToeEvent(gameOver)); } } }
// Berechne, wer im Moment der Gewinner ist, und // ob das Spiel zuende ist. private void updateGameStatus() { winner = computeWinner();
// Wenn es einen Gewinner gibt, oder es keine freien // Felder mehr gibt, dann ist das Spiel zuende if (winner != TicTacToeModel.PLAYER_NONE || !existFreeFields()) { setGameOver(true); } } // Berechnet den Gewinner - liefert PLAYER_X oder PLAYER_O, // oder PLAYER_NONE wenn es (noch) keinen Gewinner gibt. private int computeWinner() { for (int i=0; i<3; i++) { if (getPiece(i,0) != TicTacToeModel.PLAYER_NONE && equal(i,0, i,1, i,2)) { return getPiece(i,0); } if (getPiece(0,i) != TicTacToeModel.PLAYER_NONE && equal(0,i, 1,i, 2,i)) { return getPiece(0,i); } } if (getPiece(0,0) != TicTacToeModel.PLAYER_NONE && equal(0,0, 1,1, 2,2)) { return getPiece(0,0); } if (getPiece(0,2) != TicTacToeModel.PLAYER_NONE && equal(0,2, 1,1, 2,0)) { return getPiece(0,2); } return TicTacToeModel.PLAYER_NONE; }
// Gibt zurück, ob die angegebenen Felder die // gleichen Spielsteine enthalten private boolean equal(int r0, int c0, int r1, int c1, int r2, int c2) { return (getPiece(r0, c0) == getPiece(r1, c1) && getPiece(r1, c1) == getPiece(r2, c2)); }
// Gibt zurück, ob es noch freie Felder gibt private boolean existFreeFields() { for (int row=0; row<3; row++) { for (int column=0; column<3; column++) { if (board[row][column] == TicTacToeModel.PLAYER_NONE) { return true; } } } return false; }
} </code=java>
Es ist wichtig, dass Benachrichtigungen der Listener nur dann durchgeführt werden, wenn sich auch wirklich etwas geändert hat. Andernfalls könnte es passieren, dass eine endlose Kette von Benachrichtigungen entsteht, obwohl sich eigentlich nichts verändert. Ein allgemeines Schema für solche Benachrichtigungen könnte demnach so aussehen:
<code=java>
class DefaultModel implements Model
{
private int value = 0; ...
public void setValue(int newValue) { if (this.value != newValue) { this.value = newValue;
// Nur wenn sich wirklich etwas geändert hat, dürfen // die Listener informiert werden! sendEventToListeners(); } }
// Methode, die alle Listener über Änderungen benachrichtigt private void sendEventToListeners() { ... }
... } </code=java>
Ein erster Test
Das Modell ist in dieser Form schon voll funktionsfähig. Man kann sich zum Beispiel ein kleines Programm schreiben, in dem man sich ein solches TicTacToe-Spiel erstellt und einige Spielzüge durchführt. Aber natürlich kann man damit noch nicht wirklich spielen - man sieht nämlich nicht, wie das Spielfeld aussieht. Dafür benötigt man eine View:
Die View - einfachste Version
Im MVC-Muster ist die View dafür zuständig, Informationen über das Modell auf dem Bildschirm auzugeben. Die View ist dazu ein Listener, der das Modell beobachtet. Wenn das Modell geändert wird, werden alle Listener - also auch die View - benachrichtigt. Die View kann dann die Bildschirmausgabe aktualisieren.
Im einfachsten Fall gibt die View Informationen einfach auf der Konsole aus - wie in der folgenden Implementierung. Die Klasse implementiert das TicTacToeListener-Interface. Die Methoden in diesem Interface werden vom Modell aufgerufen. In diesen Methoden wird nur eine Information auf der Konsole ausgegeben, was gerade passiert ist.
<code=java> // Eine einfache View für ein TicTacToeModel: Die Klasse implementiert // das TicTacToeListener interface, und gibt Informationen über alle // Änderungen am Modell auf der Konsole aus public class TicTacToeViewConsole implements TicTacToeListener {
// Das TicTacToeModel, das angezeigt werden soll private TicTacToeModel model;
// Konstruktor public TicTacToeViewConsole(TicTacToeModel model) { this.model = model; }
// Implementierung des TicTacToeListener-Interfaces: // Die Methode wird aufgerufen, wenn ein neuer // Spielstein gesetzt wurde. Sie gibt das aktuelle // Spielfeld auf der Konsole aus. @Override public void pieceWasSet(TicTacToeEvent event) { System.out.println("Player "+event.getPlayer()+" has set "+ "a piece on "+event.getRow()+"/" + event.getColumn()); for (int row=0; row<3; row++) { for (int column=0; column<3; column++) { int piece = model.getPiece(row, column); if (piece == TicTacToeModel.PLAYER_NONE) { System.out.print("."); } else if (piece == TicTacToeModel.PLAYER_X) { System.out.print("X"); } else if (piece == TicTacToeModel.PLAYER_O) { System.out.print("O"); } } System.out.println(); } System.out.println(); }
// Implementierung des TicTacToeListener-Interfaces: // Die Methode wird aufgerufen, wenn der Spielstatus // sich geändert hat. Sie gibt den aktuellen status // aus, und ob jemand gewonnen hat. @Override public void statusChanged(TicTacToeEvent event) { if (event.isGameOver()) { System.out.println("Game over.");
int winner = model.getWinner(); if (winner == TicTacToeModel.PLAYER_NONE) { System.out.println("No winner"); } else if (winner == TicTacToeModel.PLAYER_X) { System.out.println("The winner is X."); } else if (winner == TicTacToeModel.PLAYER_O) { System.out.println("The winner is O."); } } else // event.isGameOver() == false { System.out.println("Game started"); } }
} </code=java>
Wenn man diese Klasse nun als Listener zum Modell hinzufügt, werden während des "Spiels" Informationen über den Spielablauf auf der Konsole ausgegeben:
<code=java>
public class Main
{
public static void main(String[] args) { TicTacToeModel model = new DefaultTicTacToeModel(); TicTacToeListener view = new TicTacToeViewConsole(model); model.addTicTacToeListener(view); model.setPiece(1,1, TicTacToeModel.PLAYER_X); model.setPiece(1,2, TicTacToeModel.PLAYER_O); model.setPiece(0,0, TicTacToeModel.PLAYER_X); model.setPiece(0,2, TicTacToeModel.PLAYER_O); model.setPiece(2,2, TicTacToeModel.PLAYER_X); }
} </code=java>
Um das Spiel wirklich spielen zu können, fehlt aber noch etwas...
Der oder die Controller
Im MVC-Muster soll der Controller als der "Vermittler" fungieren, der Benutzereingaben von der View an das Modell weiterreicht. Es gibt viele verschiedene Beschreibungen der genauen Arbeitsweise des Controllers. Teilweise wird der Controller auch als eine Art Relikt angesehen, das in den ersten Implementierungen des MVC-Musters noch eine klar definierte Funktion hatte, heutzutage aber nicht mehr als eigenständiges Modul angesehen werden kann oder muss.
Es gibt nur wenige Anwendungsbereiche, wo der Controller tatsächlich als eine eigenständige Klasse existieren muss - zum Beispiel, wenn bei der Kommunikation zwischen Modell und View bestimmte Bedingungen eingehalten werden müssen, wie etwa bei Web-Anwendungen. In den meisten anderen Fällen gibt es unterschiedliche Arten, wie Controller umgesetzt sein können:
- Die Hauptanwendung erstellt einen Controller, der zwischen Modell und View vermittelt
- Die Hauptanwendung [i]ist[/i] der Controller
- Der Controller ist ein Listener, der Eingaben aus der View an das Modell weiterreicht
- Der Controller ist ein Teil der View - zum Beispiel eine innere Klasse in der View
Ein einfacher Controller
Oben wurde bereits beschrieben, wie man eine View erstellen kann, die den Status des Modells auf der Konsole ausgibt. Wenn man das Spiel mit dieser View spielen will, bietet es sich an, auch eine Klasse zu haben, die Benutzereingaben von der Konsole liest und an das Modell weiterreicht. Dafür kann man eine Controller-Klasse erstellen.
Dies ist zwar keine "klassische" Controller-Klasse, da sie die Eingaben nicht von der View selbst erhält, aber zusammen mit der oben beschriebenen View ermöglicht sie es bereits, das Spiel TicTacToe an der Konsole zu spielen:
<code=java> import java.util.Scanner;
// Ein einfacher Controller für TicTacToe, der die Steuerung // und Veränderung eines TicTacToeModels über die Konsole // ermöglicht. public class TicTacToeControllerConsole {
// Ein Scanner, um Eingaben von der Konsole zu lesen private Scanner scanner = new Scanner(System.in);
// Das TicTacToeModel private TicTacToeModel model; // Konstruktor public TicTacToeControllerConsole(TicTacToeModel model) { this.model = model; } // Die Hauptmethode, die das Spiel immer neu startet // bis der Benutzer es abbricht public void play() { while (true) { playOnce(); boolean playAgain = askPlayAgain(); if (playAgain) { model.restart(); } else { System.out.println("bye"); break; } } } // Methode, die ein einzelnes Spiel durchführt private void playOnce() { int currentPlayer = TicTacToeModel.PLAYER_X;
// Solange das Spiel nicht zuende ist, wird abgefragt, // in welche Zeile und Spalte der Spieler setzen will, // und der entsprechende Zug im Modell durchgeführt. while (!model.isGameOver()) { System.out.println("Player "+currentPlayer); System.out.println("Row : "); int row = scanner.nextInt();
System.out.println("Column: "); int column = scanner.nextInt(); model.setPiece(row, column, currentPlayer); if (currentPlayer == TicTacToeModel.PLAYER_X) { currentPlayer = TicTacToeModel.PLAYER_O; } else { currentPlayer = TicTacToeModel.PLAYER_X; } } } // Fragt, ob noch einmal gespielt werden soll, und gibt // 'true' zurück, wenn 'y' oder 'Y' eingegeben wurde, oder gibt // 'false' zurück, wenn 'n' oder 'n' eingegeben wurde private boolean askPlayAgain() { while (true) { System.out.println("Play again? [y/n] :"); String answer = scanner.next(); if (answer.toLowerCase().equals("y")) { return true; } else if (answer.toLowerCase().equals("n")) { return false; } } }
} </code=java>
Man kann nun in einer main-Methode ein TicTacToeModel erstellen. An diesem Modell kann eine TicTacToeViewConsole als Listener registriert werden, so dass der Spielablauf auf der Konsole ausgegeben wird. Zusätzlich kann man nun noch den Controller erstellen, der den Spielablauf steuert:
<code=java>
public class Main
{
public static void main(String[] args) { TicTacToeModel model = new DefaultTicTacToeModel(); TicTacToeListener view = new TicTacToeViewConsole(model); model.addTicTacToeListener(view);
TicTacToeControllerConsole controller = new TicTacToeControllerConsole(model); controller.play(); }
} </code=java>
Die View - graphische Version
Oben wurde bereits eine View erstellt, die den Spielverlauf auf der Konsole ausgibt. Nun soll eine graphische View erstellt werden, die das Spiel in einem Swing-Fenster darstellt. Die View enthält ein Panel, das ein Spielfeld zeichnen kann. Dieses Panel erhält die Informationen, wo welcher Spieler welche Steine gesetzt hat, aus dem Modell. Auch diese View implementiert das TicTacToeListener interface. Sie kann damit als Listener zum Modell hinzugefügt werden. Wenn sich am Modell etwas ändert, wird die View benachrichtigt, und sie kann den neuen Spielzustand zeichnen.
<code=java> import java.awt.*; import java.awt.event.*;
import javax.swing.*;
// Eine einfache graphische View für ein TicTacToeModel public class TicTacToeViewGUI extends JPanel implements TicTacToeListener {
// Das Modell, das angezeigt werden soll private TicTacToeModel model;
// Ein Panel, auf das das Spielfeld gezeichnet wird private TicTacToePanel panel; // Ein Label für Nachrichten private JLabel messageLabel; // Konstruktor public TicTacToeViewGUI(final TicTacToeModel model) { this.model = model; setLayout(new BorderLayout());
messageLabel = new JLabel(" "); add(messageLabel, BorderLayout.NORTH); panel = new TicTacToePanel(); panel.setPreferredSize(new Dimension(300,300)); add(panel, BorderLayout.CENTER); }
// Implementierung des TicTacToeListener-Interfaces: // Die Methode wird aufgerufen, wenn ein neuer // Spielstein gesetzt wurde. @Override public void pieceWasSet(TicTacToeEvent event) { String message = "Player "+event.getPlayer()+ " set on "+event.getRow()+"/" + event.getColumn(); messageLabel.setText(message); panel.repaint(); }
// Die Methode wird aufgerufen, wenn der Spielstatus // sich geändert hat. @Override public void statusChanged(TicTacToeEvent event) { String message = ""; if (event.isGameOver()) { message = "Game over. ";
int winner = model.getWinner(); if (winner == TicTacToeModel.PLAYER_NONE) { message += "No winner"; } else if (winner == TicTacToeModel.PLAYER_X) { message += "The winner is X."; } else if (winner == TicTacToeModel.PLAYER_O) { message += "The winner is O."; } } else // event.isGameOver() == false { message = "Game started"; } messageLabel.setText(message); panel.repaint(); }
// Eine innere Klasse, die ein TicTacToe-Spielfeld zeichnet class TicTacToePanel extends JPanel { @Override public void paintComponent(Graphics g) { super.paintComponent(g);
int dx = getWidth() / 3; int dy = getHeight() / 3; // Male das Gitter g.setColor(Color.BLACK); g.drawLine(1*dx,0,1*dx,getHeight()); g.drawLine(2*dx,0,2*dx,getHeight()); g.drawLine(0,1*dy,getWidth(),1*dy); g.drawLine(0,2*dy,getWidth(),2*dy); for (int row=0; row<3; row++) { for (int column=0; column<3; column++) { // Male an die Gitterpositionen, an denen schon // Spielsteine liegen, Kreuze oder Kreise int piece = model.getPiece(row, column); if (piece == TicTacToeModel.PLAYER_X) { paintCross(g, column*dx, row*dy, dx, dy); } else if (piece == TicTacToeModel.PLAYER_O) { paintCircle(g, column*dx, row*dy, dx, dy); } } } } private void paintCross(Graphics g, int x0, int y0, int dx, int dy) { g.drawLine(x0,y0,x0+dx,y0+dy); g.drawLine(x0,y0+dy,x0+dx,y0); }
private void paintCircle(Graphics g, int x0, int y0, int dx, int dy) { g.drawOval(x0, y0, dx, dy); } }
} </code=java>
Um diese View zu verwenden, kann das vorherige Beispiel um eine Methode erweitert werden, die das TicTacToeViewGUI erstellt und dem Modell als Listener hinzufügt. Damit ist es schon möglich, das Spiel wie bisher an der Konsole zu spielen, aber zusätzlich die graphische Ausgabe in einem Fenster zu erhalten:
<code=java> public class Main {
public static void main(String[] args) { final TicTacToeModel model = new DefaultTicTacToeModel(); TicTacToeListener view = new TicTacToeViewConsole(model); model.addTicTacToeListener(view);
createGUI(model); TicTacToeControllerConsole controller = new TicTacToeControllerConsole(model); controller.play(); } private static void createGUI(final TicTacToeModel model) { try { // GUI-Komponenten müssen auf dem Event-Dispatch-Thread // erstellt und angezeigt werden SwingUtilities.invokeAndWait(new Runnable() { public void run() { TicTacToeViewGUI gui = new TicTacToeViewGUI(model); model.addTicTacToeListener(gui);
JFrame frame = new JFrame(); frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); frame.getContentPane().add(gui); frame.pack(); frame.setVisible(true); } }); } catch (InterruptedException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } }
} </code=java>
Controller für die graphische View
Natürlich will man das Spiel nicht immer nur an der Konsole spielen: Man möchte das Spiel mit dem graphischen GUI steuern, die Spielsteine durch Mausklicks setzen, das Spiel mit einem Button neu starten oder beenden können. Dazu müssen in der graphischen View-Klasse einige Erweiterungen gemacht werden: Es wird ein Button eingefügt, mit dem das Spiel neu gestartet werden kann. Der Button bekommt dazu einen ActionListener, der das Modell zurücksetzt, wenn der Button gedrückt wird. Zusätzlich wird ein MouseListener mit dem Panel verbunden, das das Spielfeld zeichnet. Dieser MouseListener berechnet aus der Position eines Mausklicks, in welches Feld der aktive Spieler einen Spielstein setzen will.
Die entsprechend erweiterte View-Klasse, bei der im Konstruktor ein zusätzlicher Button erstellt wird, und eine innere Klasse "BoardMouseListener" die Mausklicks auf dem Spielfeld in Spielzüge umsetzt:
<code=java> import java.awt.*; import java.awt.event.*;
import javax.swing.*;
// Eine einfache graphische View für ein TicTacToeModel public class TicTacToeViewGUI extends JPanel implements TicTacToeListener {
// Das Modell, das angezeigt werden soll private TicTacToeModel model;
// Ein Panel, auf das das Spielfeld gezeichnet wird private TicTacToePanel panel; // Ein Label für Nachrichten private JLabel messageLabel; // Der aktive Spieler private int activePlayer = TicTacToeModel.PLAYER_X;
// Konstruktor public TicTacToeViewGUI(final TicTacToeModel model) { this.model = model; setLayout(new BorderLayout());
messageLabel = new JLabel(" "); add(messageLabel, BorderLayout.NORTH); panel = new TicTacToePanel(); panel.setPreferredSize(new Dimension(300,300)); add(panel, BorderLayout.CENTER);
JButton restartButton = new JButton("Restart"); restartButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { model.restart(); activePlayer = TicTacToeModel.PLAYER_X; } }); add(restartButton, BorderLayout.SOUTH); panel.addMouseListener(new BoardMouseListener()); }
// Implementierung des TicTacToeListener-Interfaces: // Die Methode wird aufgerufen, wenn ein neuer // Spielstein gesetzt wurde. @Override public void pieceWasSet(TicTacToeEvent event) { String message = "Player "+event.getPlayer()+ " set on "+event.getRow()+"/" + event.getColumn(); messageLabel.setText(message); panel.repaint(); }
// Die Methode wird aufgerufen, wenn der Spielstatus // sich geändert hat. @Override public void statusChanged(TicTacToeEvent event) { String message = ""; if (event.isGameOver()) { message = "Game over. ";
int winner = model.getWinner(); if (winner == TicTacToeModel.PLAYER_NONE) { message += "No winner"; } else if (winner == TicTacToeModel.PLAYER_X) { message += "The winner is X."; } else if (winner == TicTacToeModel.PLAYER_O) { message += "The winner is O."; } } else // event.isGameOver() == false { message = "Game started"; } messageLabel.setText(message); panel.repaint(); }
// Eine innere Klasse, die ein TicTacToe-Spielfeld zeichnet class TicTacToePanel extends JPanel { @Override public void paintComponent(Graphics g) { super.paintComponent(g);
int dx = getWidth() / 3; int dy = getHeight() / 3; // Male das Gitter g.setColor(Color.BLACK); g.drawLine(1*dx,0,1*dx,getHeight()); g.drawLine(2*dx,0,2*dx,getHeight()); g.drawLine(0,1*dy,getWidth(),1*dy); g.drawLine(0,2*dy,getWidth(),2*dy); for (int row=0; row<3; row++) { for (int column=0; column<3; column++) { // Male an die Gitterpositionen, an denen schon // Spielsteine liegen, Kreuze oder Kreise int piece = model.getPiece(row, column); if (piece == TicTacToeModel.PLAYER_X) { paintCross(g, column*dx, row*dy, dx, dy); } else if (piece == TicTacToeModel.PLAYER_O) { paintCircle(g, column*dx, row*dy, dx, dy); } } } } private void paintCross(Graphics g, int x0, int y0, int dx, int dy) { g.drawLine(x0,y0,x0+dx,y0+dy); g.drawLine(x0,y0+dy,x0+dx,y0); }
private void paintCircle(Graphics g, int x0, int y0, int dx, int dy) { g.drawOval(x0, y0, dx, dy); } } class BoardMouseListener extends MouseAdapter implements MouseListener { @Override public void mouseClicked(MouseEvent mouseEvent) { if (!model.isGameOver()) { Component component = mouseEvent.getComponent(); int width = component.getWidth(); int height = component.getHeight(); int row = mouseEvent.getY() / (height / 3); int column = mouseEvent.getX() / (width / 3); model.setPiece(row, column, activePlayer); if (activePlayer == TicTacToeModel.PLAYER_X) { activePlayer = TicTacToeModel.PLAYER_O; } else { activePlayer = TicTacToeModel.PLAYER_X; } } } }
} </code=java>
In diesem Fall gibt es nicht einen Controller, sondern mehrere, die ganz bestimmte Aufgaben erledigen. Der erste Controller ist der (anonyme) ActionListener, der an den Button gehängt wird. Er registriert, wenn der Benutzer auf den Button klickt, um das Spiel neu zu starten, und ruft die restart-Methode des Modells auf. Der zweite Controller ist der BoardMouseListener, der an das Spielfeld gehängt wird. Er regstriert Mausklicks, und setzt im Modell die Spielsteine an die entsprechenden Stellen.
Dieser Teil des MVC-Patterns könnte auch anders umgesetzt werden - je nach gewünschter Funktion des Controllers, und abhängig davon, welche zusätzlichen Aufgaben der Controller übernehmen soll. Im speziellen könnte es sinnvoll sein, eine eigene Controller-Klasse zu erstellen. Der Vorteil daran wäre, dass zusätzliche Verwaltungsaufgaben, die nichts mit der Darstellung (also der View) zu tun haben, in einer eigenen Klasse liegen könnten.
Um die Verbindung zwischen der View und dem Controller herzustellen, gäbe es verschiedene Möglichkeiten. Die GUI-Komponenten könnten nach außen sichtbar gemacht werden. Der Nachteil dabei wäre, dass die Kapselung der View aufgegeben wird, und die GUI-Komponenten von außerhalb der View-Klasse verändert werden könnten. Um das zu verhindern könnte man in der View lediglich Methoden anbieten, die es erlauben, Listener an die Komponenten zu hängen. Doch auch hierbei entstünde eine starke Abhängigkeit zwischen View und Controller, und Änderungen an der GUI könnten zwangsläufig Änderungen am Controller nach sich ziehen.
Um solche Abhängigkeiten weiter zu minimieren, und trotzdem die Flexibilität zu erhöhen, könnte man für den Controller - und gegebenenfalls auch für die View - wiederum eigene Interfaces definieren, die die minimalen Anforderungen für diese Module zusammenfassen. Diese und andere Erweiterungen und Verallgemeinerungen, und das dabei entstehende Zusammenspiel dieser Module sind aber vom konkreten Anwendungsfall abhängig.
Visitor
Mit dem Visitor-Pattern können Elemente einer komplexeren Datenstruktur besucht werden.
Hat man eine Datenstruktur wie einen Baum (oder einen Graphen), so möchte man aus verschiedensten Gründen alle Knoten besuchen, und irgendetwas mit den Knoten berechnen (sie z.B. zählen).
Die Knoten können aber durch mehrere Klassen beschrieben werden, und natürlich gibt es verschiedene Gründe alle Knoten zu besuchen.
Ein Algorithmus, der alle Knoten besucht und mit ihnen etwas macht, kann man folgendermaßen aufteilen:
Den Teil der nur die einzelnen Knoten sucht, und den Teil der effektiv etwas mit ihnen macht.
Der erste Teil ist immer derselbe, der zweite muss für jede Knoten-Klasse, und für jeden Algorithmus von Neuem geschrieben werden.
Das Visitor-Pattern erlaubt diese Trennung: die Traversierung der Knoten wird an einem zentralen Ort beschrieben.
Jedem Knoten wird ein Visitor-Interface übergeben, wo er eine zu ihm passende Methode aufrufen soll: Der Knoten des Types X ruft die Methode "handleX( X x )
" auf (mit x = sich selbst).
Natürlich kann das Objekt, das hinter dem Interface steht, beliebig oft ausgetauscht werden. Man kann also neue Algorithmen implementieren, ohne den Code der Knoten-Klassen überhaupt zu kennen!
- Der Baum stellt eine Möglichkeit zur Verfügung, den Visitor allen Knoten zu überreichen.
- Die Knoten übergeben sich selbst einer bestimmten Methode des Visitors.
- Der Visitor stellt für jeden Typ Knoten eine Methode zur Verfügung, in der er den Knoten in irgendeiner Weise verarbeitet.
Beispiel
Eine Gleichung "2+4+3" kann man als Baum darstellen. Die Operatoren sind Knoten, die Zahlen sind die Blätter des Baumes.
Eine entsprechende Datenstruktur, bereits mit Visitor, sieht so aus: <code=java>/**
* Der Besucher, der durch den Baum gereicht wird. */
public interface Visitor{
/** * Behandelt einen Operator-Knoten. * @param operator Der Operator. */ public void handleOperator( Operator operator ); /** * Behandelt einen Number-Knoten. * @param number Die Nummer */ public void handleNumber( Number number );
//----- Nur damit das Beispiel eine schöne Ausgabe kriegt, --------------------/ //----- die Methode "getResult" gehört normalerweise nicht zu einem Visitor. --/
/** * Das Resultat des Besuches. * @return Das Resultat */ public String getResult();
}</code=java> <code=java>/**
* Jedes Element eines Baumes ist ein Knoten. Ein Blatt kann einfach als * Knoten ohne Kinder angeschaut werden. */
public interface Node{
public Node[] getChildren(); /** * Ruft die passende Methode des Visitors auf. * @param visitor Der Besucher. */ public void visit( Visitor visitor );
}</code=java> <code=java>/**
* Ein Operator ist ein Knoten, der zwei Kinder hat. */
public class Operator implements Node{
private Node[] children; private String name; public Operator( Node left, String name, Node right ){ this.name = name; children = new Node[]{ left, right }; } public Node[] getChildren() { return children; } public void visit( Visitor visitor ) { visitor.handleOperator( this ); } public String getName(){ return name; }
}</code=java> <code=java>/**
* Eine Zahl ist ein Blatt, also ein Knoten ohne Kinder */
public class Number implements Node{
public static final Node[] EMPTY = new Node[0]; private double value; public Number( double value ){ this.value = value; } public Node[] getChildren() { return EMPTY; } public void visit( Visitor visitor ) { visitor.handleNumber( this ); } public double getValue(){ return value; }
}</code=java>
Für verschiedene Aktionen: Summe aller Zahlen, Anzahl Elemente der Gleichung, sowie eine Liste verschiedener Operatoren, benötigt nun sehr wenig Code:
<code=java>import java.util.Arrays; import java.util.HashSet; import java.util.Set;
public class Test{
public static void main( String[] args ) { // 5 * 3 + 9 + 6 Node equation = new Operator( new Operator( new Number( 5 ), "*", new Number( 3 ) ), "+", new Operator( new Number( 9 ), "+", new Number( 6 ) )); // Summer alle Zahlen System.out.println( traverse( equation, new Visitor(){ private double sum = 0; public void handleOperator( Operator operator ) { } public void handleNumber( Number number ) { sum += number.getValue(); } public String getResult() { return "Summe aller Zahlen: " + sum; } })); // Anzahl Knoten System.out.println( traverse( equation, new Visitor(){ private int count; public void handleOperator( Operator operator ) { count++; } public void handleNumber( Number number ) { count++; } public String getResult() { return "Anzahl Knoten: " + count; } })); // Verschiedene Operatoren System.out.println( traverse( equation, new Visitor(){ private Set<String> operators = new HashSet<String>(); public void handleOperator( Operator operator ) { operators.add( operator.getName() ); } public void handleNumber( Number number ) { } public String getResult() { return "Verschiedene Operatoren: " + Arrays.toString( operators.toArray() ); } })); } public static String traverse( Node node, Visitor visitor ){ node.visit( visitor ); for( Node child : node.getChildren() ) traverse( child, visitor ); return visitor.getResult(); }
}</code=java>
Ausgabe: <code=ini>Summe aller Zahlen: 23.0 Anzahl Knoten: 7 Verschiedene Operatoren: [+, *]</code=ini>
-- bygones 30.06.2004, | Bleiglanz | Illuvatar 02.09.2004 | Beni 16.07.2005 | [[Benutzer:André Uhres | André Uhres] 21.11.2009 | [[Benutzer:Marco13 | Marco13] 30.12.2009