Entwurfsmuster (Design Patterns)

Aus Byte-Welt Wiki
Zur Navigation springenZur Suche springen

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.

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

Das Read-Only Interface stellt sicher, dass nur bestimmte priviligierte Klassen eine Instanz ändern können.

Das Read-Only Interface ist dem Immutable Pattern ähnlich, nur dass hier bestimmte Klassen das Recht haben eine Instanz zu ändern.

Die "halb-immutable" Klasse nennen wir Mutable, Klassen die ßnderungen vornehmen dürfen nennen wir Mutator. Wichtig ist das die beiden Klassen in einem package liegen, da die setter Methoden der Mutable Klasse über den Zugriffsoperator package private definiert werden, so dass nur Klassen im selben Package darauf zugreifen können. Die Mutable Klasse implementiert das sog. Read-Only Interface das die getter Methoden definiert. Alle "unpriviligierten" Klassen arbeiten nur über das Interface und haben keinen direkten Zugriff auf die Mutable Klasse

Beispiel: <code=java>package pattern.readonly;

public interface Person {

   public String getName();

}[/java] [java]package pattern.mutable;

import pattern.readonly.ReadOnlyInterface;

public class MutablePerson implements Person {

   public String name;
   
   public MutablePerson(String s) {
       name = s;
   }
   
   public String getName() {
       return name;
   }
   
   void setName(String s) {
       name = s;
   }

}</code=java>

<code=java>package pattern.readonly;

public class UnpriviligedClass {

   public UnpriviligedClass(Person p) {
       // Kann nur auf die get Methode zugreifen
      System.out.println(p.getName());
   }

}</code=java>

<code=java>package pattern.mutable;

public class Mutator {

   private MutablePerson mutable;
   public Mutator(MutablePerson mp) {
       mutable = mp;
       // Kann auch auf die set Methoden zugreifen
       mutable.setName("bygones");
   }

}</code=java>

Häufig setzt man das Pattern ein, wenn man Daten an eine GUI sendet, die diese dann anzeigen, aber selbst keine Veränderungen vornehmen soll.

Factory

Das Factory - Pattern kann verwendet werden, wenn es nicht möglich und/oder sinnvoll ist, Instanzen mit new zu erzeugen (es könnten ja langwierige Konfigurationen vorgenommen werden müssen). So kann zum Beispiel für gleiche Objekte auch leicht die gleiche Instanz verwendet werden.

Bei diesem gibt es meist eine statische Methode, um das jeweilige Objekt zu erzeugen. Ein Singleton ist eine Form des Factory-Patterns, wobei hier die Factory - Methode sicherstellt, dass es nur eine Instanz gibt.

Es gibt verschiedene Möglichkeiten, wo sich die Factory-Methode befinden kann.

  • In der eigenen Klasse:

<code=java>public class Foo {

 private Foo () //Foo soll nur von der factory-Methode instanziiert werden
 {
 }
 public static Foo getFoo (Settings s) //statische factory - Methode
 {
   Foo bar = new Foo();
   //Wende s auf bar an
   return bar;
 }

}</code=java>

Das wird zum Beispiel in der Klasse java.net.InetAddress angewandt.

  • In einer factory - Klasse:

<code=java> public class Foo {

 Foo() //Konstruktor nur für package zugänglich, da factory Methode in andere Klasse im selben Package liegt
 {
 }

}

public class Bar {

 Bar() //Konstruktor nur für package zugänglich, da factory Methode in andere Klasse im selben Package liegt
 {
 }

}

public class FooBarFactory {

 public static Foo getFoo (Settings s) //Factory - Methode für Foo
 {
   Foo f = new Foo(); //erlaubt
   //intialisiere f
   return f;
 }
 public static Bar getBar (Settings s) //Factory - Methode für Bar
 {
   Bar b = new Bar(); //erlaubt
   //intialisiere b
   return b;
 }

} </code=java>

  • Mit Hilfe einer "Abstract Factory":

[Bild http://www2.informatik.hu-berlin.de/~obecker/hjp3/images/AbstractFactory.gif]

Das Grundkonzept sieht so aus, wie bei der Factory-Klasse oben. Allerdings sind in diesem Falle alle Klassen abstrakt. Es kann jetzt sowohl von der Factory-Klasse als auch von den faktorisierten (Achtung, meine Wortschöpfung) Klassen mehrere unterschiedliche Ableitungen A, B, C... geben. Die Klasse FooBarFactoryA stellt dann eben FooA und BarA her, und FooBarFactoryB stellt FooB und BarB her. Am deutlichsten macht das folgender Beispielcode aus dem Javabuch: <code=java>/* Listing1010.java */

//------------------------------------------------------------------ //Abstrakte Produkte //------------------------------------------------------------------ abstract class Product1 { }

abstract class Product2 { }

//------------------------------------------------------------------ //Abstrakte Factory //------------------------------------------------------------------ abstract class ProductFactory {

 public abstract Product1 createProduct1();
 public abstract Product2 createProduct2();
 public static ProductFactory getFactory(String variant)
 {
   ProductFactory ret = null;
   if (variant.equals("A")) {
     ret = new ConcreteFactoryVariantA();
   } else if (variant.equals("B")) {
     ret = new ConcreteFactoryVariantB();
   }
   return ret;
 }
 public static ProductFactory getDefaultFactory()
 {
   return getFactory("A");
 }

}

//------------------------------------------------------------------ //Konkrete Produkte für Implementierungsvariante A //------------------------------------------------------------------ class Product1VariantA extends Product1 { }

class Product2VariantA extends Product2 { }

//------------------------------------------------------------------ //Konkrete Factory für Implementierungsvariante A //------------------------------------------------------------------ class ConcreteFactoryVariantA extends ProductFactory {

 public Product1 createProduct1()
 {
   return new Product1VariantA();
 }
 public Product2 createProduct2()
 {
   return new Product2VariantA();
 }

}

//------------------------------------------------------------------ //Konkrete Produkte für Implementierungsvariante B //------------------------------------------------------------------ class Product1VariantB extends Product1 { }

class Product2VariantB extends Product2 { }

//------------------------------------------------------------------ //Konkrete Factory für Implementierungsvariante B //------------------------------------------------------------------ class ConcreteFactoryVariantB extends ProductFactory {

 public Product1 createProduct1()
 {
   return new Product1VariantB();
 }
 public Product2 createProduct2()
 {
   return new Product2VariantB();
 }

} </code=java>

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