Entwurfsmuster (Design Patterns)

Aus Byte-Welt Wiki
Version vom 8. März 2018, 19:57 Uhr von L-ectron-X (Diskussion | Beiträge)
(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)
Zur Navigation springenZur Suche springen
Dieser Artikel überschneidet sich mit einigen Artikeln der Kategorie Design Pattern

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:

public class Singleton {
  public static final Singleton instance = new Singleton();

  private Singleton() {
  }
}
public class Singleton {
  private static final Singleton instance = new Singleton();
  
  private Singleton() {
  }
		
  public static Singleton getInstance() {
    return instance;
  }
}

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:

public class Singleton {
  private static Singleton instance;
  
  private Singleton() {
  }
		
  public static synchronized Singleton getInstance() {
    if (instance == null) {
        instance = new Singleton();
    }
    return instance;
  }
}

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:

import java.util.Observable;
import java.util.Observer;

import javax.swing.JFrame;

/**
 * Die Klasse <tt>WetterAnzeige</tt> ist die GUI um die <tt>Wetterdaten</tt>
 * 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 <tt>update</tt> vom Interface <tt>Observer</tt> wird
   * aufgerufen wenn ein <tt>Observer</tt> <tt>notifyObserver</tt>
   * aufruft. Der <tt>Observer</tt> kann sich bei mehreen
   * <tt>Observable</tt> 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);
  }
}

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:

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");
  }
}

Hingegen wäre richtig:

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");
  }
}

Iterator

Das Iterator Pattern erlaubt den sequentiellen Zugriff auf eine Aggregation ohne deren internen Struktur zu kennen.

Das Iterator Pattern ist eins der bekanntesten und weitverbreitesten Pattern in Java. Der Client muss nicht wissen wie die interne Struktur der Aggregation aussieht bzw. wie die Elemente angeordnet sind, kann aber über einen Iterator auf diese Element zugreifen.

Der Iterator ist meist ein Interface der die beiden methoden "next()" and "hasNext()" definiert, wobei hasNext() testet ob noch auf Elemente zugegriffen werden kann und next() liefert dann das nächste Element.

Beispiel:

package pattern;

interface IntIterator {
    public boolean hasNext();

    public int nextInt();
}

public class IntListe {
    private int[] array;

    public IntListe(int[] a) {
        array = a;
    }

    public IntIterator iterator() {
        return new IntIterator() {
            int index = 0;

            public boolean hasNext() {
                return index < array.length;
            }

            public int nextInt() {
                return array[index++];
            }
        };
    }

    public IntIterator backIterator() {
        return new IntIterator() {
            int index = array.length - 1;

            public boolean hasNext() {
                return index > -1;
            }

            public int nextInt() {
                return array[index--];
            }

        };
    }

    public static void main(String[] args) {
        int[] a = { 1, 2, 3, 4, 5 };
        IntListe il = new IntListe(a);
        for (IntIterator iter = il.iterator(); iter.hasNext();) {
            int next = iter.nextInt();
            System.out.print(next + " ");
        }
        System.out.println();
        for (IntIterator iter = il.backIterator(); iter.hasNext();) {
            int next = iter.nextInt();
            System.out.print(next + " ");
        }
    }
}

Das Pattern ist in Java fest integriert und kann über den aufruf iterator() auf jede Collection angewandt werden.

Fasade

Das Fassade erlaubt den einfachen Zugriff auf ein komplexes Subsystem.

Ein Programm unterteilt sich meist in Subsysteme. Je nach Größe und Komplexität dieser Systeme ist es manchmal schwierig zu verstehen welche Methoden wie und warum in diesem System definiert wurden. Lässt man Clients nun auf die einzelnen Methoden des Subsystems zugreifen erschwert das die Wartung des kompletten Systems, da bei jeder ßnderung des Subsystems alle nach außen gehenden Assoziation getestet werden müssen.

Daher erschafft man sich eine sog. Fassaden Klasse die einige der wichtigen bzw. benötigten public Methoden des Subsystems definieren. Clients greifen nun nicht direkt in Klassen des Subsystems, sondern nutzen nur die Fassaden Klasse. Das macht das gesamte System stabiler da weniger Abhängigkeiten existieren und dadurch auch die Wartbarkeit verbessert wird.

General Hierarchie

Das General Hierarchie Pattern hilft beim modellieren von Objekten die in einer Hierarchischen Struktur zu finden sind.

Jedes Objekt in einer Hierarchie kann einen Vorgänger (superior) haben oder einen Nachfolger (subordinate). Manche Elemente haben beides, manche haben nur einen "superior". Um dies zu modellieren hilft das General Hierarchie Pattern.

Betrachtet man einen Baum so existieren Knoten (node) und Blätter (leaf). Knoten können wieder Knoten bzw. Blätter haben, Blätter hingegen haben keine Nachfolger. Um dies zu modellieren erstellt man sich eine abstrakte Klasse, die die gemeinsamen Eigenschaften der beiden Klassen vereinigt (TreeElement). Die beiden Klassen Node und Leaf erben von TreeElement, wobei Node eine mehrfach Assoziation wieder zur Oberklasse "TreeElement" hat, was impliziert dass ein Knoten wiederum Knoten oder Blätter besitzen kann.

Ein weiteres Beispiel des Patterns ist das Verhältnis von Angestellten und Manager. Ein Manager ist ein Angestellter in einer Firma, der wiederum Angestellte unter sich hat.

Player Role Pattern

Das Player Role Pattern ordnet einer Klasse verschiedene Rollen zu.

Das Pattern lässt sich am einfachsten an einem Beispiel erklären.

Ein Internet User kann zur selben Zeit unterschiedliche Eigenschaften (Rollen) besitzen. Zum einen kann man den User unter dem Aspekt betrachten, wie er ins Internet gelangt. So kann er zum Beispiel per Flatrate das Internet nutzen oder sich per Modem einwählen. Weiterhin kann er aber auch in einem Board aktiv sein. Dort kann er entweder User, Moderator oder Admin sein.

Also kann ein sog. Player (der Internetuser) gleichzeitig mehrere untersch. Rollen einnehmen, die sich im Laufe der Zeit auch ändern können.

Der erste Ansatzpunkt dies zu lösen ist Vererbung. Doch stößt man hier schnell an die Grenzen, da Java keine Mehrfachvererbung zulässt. Da aber die Rollen auch eigenständige Objekte sind will man sie in eigene Klassen packen.

Die Lösung dazu ist es für jede Rolle eine abstrakte Oberklasse zu erstellen die eine Assoziation zu dem Player hat. Dadurch kann der Player gleichzeitig mehrere Rollen einnehmen, die sich auch während des Programms ändern können.

Immutable

Das Immutable Pattern stellt sicher, dass eine Instanz der Klasse nach der Initialisierung nicht mehr geändert werden kann.

Um eine Klasse immutable zu machen müssen folgende Punkte eingehalten werden:

  • Es darf keine Methoden geben die die Instanz verändern können, will eine Methode die Instanz ändern, so muss eine neue Instanz erstellt werden.
  • Es muss sicher gestellt werden dass Methoden nicht überschrieben werden können (z.b. die Klasse final deklarieren)
  • Alle Instanzvariable werden private final deklariert
  • Wenn die Klasse ein mutables Object hält, muss sichergestellt werden dass der Client nicht direkt auf die Referenz diese Objekts zugreifen kann.

Beispiel:

import java.util.BitSet;

public final class Immutable {
    private final BitSet set;

    /**
     * Hier wird ein neues BitSet angelegt, clone() sollte nicht verwendet
     * werden, da Parameter pSet auch eine SubKlasse von BitSet sein kann und
     * daher womöglich nicht das korrekte clone Ergebnis liefern würde
     */
    public Immutable(BitSet pSet) {
        this.set = new BitSet();
        // alle gesetzten Bits des Parameters werden im set auch gesetzt
        for (int i = pSet.nextSetBit(0); i >= 0; i = pSet.nextSetBit(i + 1)) {
            set.set(i);
        }
    }

    /**
     * Hier kann geklont werden, da man weiß das es sich um ein BitSet handelt
     */
    public BitSet getSet() {
        return (BitSet) set.clone();
    }

    /**
     * Methode ändert den Zustand der Instanz, daher muss eine neue Instanz
     * erstellt werden
     */
    public Immutable bitAnd(BitSet set) {
        set.and(this.set);
        return new Immutable(set);
    }

    public String toString() {
        return set.toString();
    }
}

Die Klasse wird als final deklariert, so dass keine Klassen von ihr erben können und somit die Methoden überschreiben könnten.

Der Konstruktor und die Methode 'getSet()' verdienen genauerer Betrachtung. Würde man z.b. im Konstruktor schreiben:

public Immutable(Bitset pSet) {
	set = pSet;
}

könnte man folgendes machen:

public static void main(String[] args) {
	BitSet set = new BitSet();
	// erstes bit wird gesetzt
        set.set(1);
        // immutable objekt wird erstellt
        Immutable im = new Immutable(set);
        // ausgabe {1};
        System.out.println(im);
        // alle bits im set werden auf false gesetzt
        set.clear();
        // ausgabe {};
        System.out.println(im);
}

d.h. man kann über das mutable BitSet Objekt das eigentlich immutable Objekt ändern, da sie mit den gleichen Referenzen arbeiten.

Das gleiche gilt für die Methode 'getSet()' - würde man schreiben:

public BitSet getSet() {
	return set;
}

könnte man folgendes machen:

public static void main(String[] args) {
        BitSet set = new BitSet();
        set.set(1);
        Immutable im = new Immutable(set);
        System.out.println(im);
        set = im.getSet();
        set.clear();
        System.out.println(im);
}

auch hier wird die immutable Instanz geändert! Daher müssen sog. 'defensive copies' von mutable Objekte verwendet werden.

Nun stellt sich die Frage warum dieser Aufwand ?

Zitat aus 'Effective Java - Programming Language Guide' von Joshua Bloch: "Classes should be immutable unless there's a very good reason to make then mutable."

Aus folgenden Gründe sollte man immutable Klassen verwenden:

  • Sie sind einfach: Die Objekte haben keine großen bzw. komplexen Zustandsraum
  • Sie sind Thread sicher: man muss z.b.kein synchronized verwenden
  • Sie sind vertrauenswürdig: Andere Objekte können sich darauf verlassen, dass diese Objekte immer so sein werden wie sie erstellt wurden. Das macht sie z.b. zu perfekten Keys in einer Map oder Elemente in einem Set. Da ihr Zustand sich nie ändern kann läuft man nicht Gefahr dass das mapping oder die Reihenfolge eines Sets sich ändert.


Ein offensichtlicher Nachteil ist, dass man möglicherweise eine große Anzahl von Instanzen benötigt, die möglicherweise noch kostenspielig sind zu Initiieren. Dies ist aber bei kleineren Objekte vernachlässigbar und man kann z.B. eine mutable Variante der Klasse bereitstellen um diese Einbußen wegzubekommen (siehe String <-> StringBuffer/StringBuilder).

Als Faustregel kann gesehen werden:

  • kleine Model - Klassen sollten immer immutable sein (v.a. Klassen ohne mutable Objekte)
  • bei größere Model - Klassen sollte man es in Betracht ziehen. Möglicherweise eine mutable Variante bereitstellen
  • falls eine Klasse nicht immutable gemacht werden kann, so sollte man den Grad der Veränderbarkeit versuchen zu minimieren.

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:

package pattern.readonly;

public interface Person {
    public String getName();
}
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;
    }
}
package pattern.readonly;

public class UnpriviligedClass {
    public UnpriviligedClass(Person p) {
        // Kann nur auf die get Methode zugreifen
       System.out.println(p.getName());
    }
}
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");
    }
}

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:
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;
  }
}

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

  • In einer factory - Klasse:
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;
  }
}
  • 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:

/* 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();
  }
}

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.

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);
    }
}
package test.model;
 
/**
 * @author deathbyaclown
 */
public enum Direction {
    NORTH, EAST, SOUTH, WEST
}
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()));
    }
}
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);
    }
}

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:

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();
}

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:

    /**
     * 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);

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:

public interface TicTacToeModel
{
    ...

    void addTicTacToeListener(TicTacToeListener listener);
    void removeTicTacToeListener(TicTacToeListener listener);
}

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:

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);
}

Die Objekte, die an diese Methoden übergeben werden, sind von einer eigenen "Event"-Klasse, die die Änderungen genauer beschreibt:

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!)
}


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:

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;
    }
}


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:

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() { ... }
...
}

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.

// 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");
        }
    }
}


Wenn man diese Klasse nun als Listener zum Modell hinzufügt, werden während des "Spiels" Informationen über den Spielablauf auf der Konsole ausgegeben:

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);
    }
}


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 ist 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:

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;
            }
        }
    }
}


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:

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();
    }
}

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.

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);
        }
    }
}


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:

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();
        }
    }
    
}

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:

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;
                }
            }
        }
    }
    
}

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:

/**
 * 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();
}
/**
 * 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 );
}
/**
 * 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;
    }
}
/**
 * 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;
    }
}

Für verschiedene Aktionen: Summe aller Zahlen, Anzahl Elemente der Gleichung, sowie eine Liste verschiedener Operatoren, benötigt nun sehr wenig Code:

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();
    }
}

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 | André Uhres 21.11.2009 | Marco13 30.12.2009