MVC

Status
Nicht offen für weitere Antworten.

André Uhres

Top Contributor
Nachfolgend habe ich den FAQ Beitrag MVC mal auf einfache Art angepasst:



MVC - Design


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.

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.

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

Java:
package test.model;
 
/**
 * @author deathbyaclown
 */
public enum Direction {
    NORTH, EAST, SOUTH, WEST
}

Java:
package test.view;

/**
 * View
 * @author deathbyaclown/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()));
    }
}

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

Marco13

Top Contributor
Das hatte ich irgendwann mal angefangen. Zwischendrin hatte ich dann noch gesehen, dass es ja schon ein MVC-Tutorial gibt, aber ... jetzt hab' ich's trotzdem mal "fertig" gemacht. Vielleicht ist es ja zumindest hinreichend für eins der Adventskalenderbücher (muss mir bei Gelgegenheit nochmal alle ansehen... gibt's eigentlich eine Deadline, bis wann man sich ein Buch ausgesucht haben muss?).

Und jetzt: Zerreißt es :)

Moderatoren können darin rumeditieren, wie sie es für richtig halten, und sonst können Kritik, Kommentare, Flamewars, usw. ja hier gepostet und ggf. (nach ausgiebiger Diskussion) eingebaut werden.

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 [c]interface[/c] 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:
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();
}

Wichtig: Das ist eine stark vereinfachte Darstellung. In einer realen Anwendung müßte man das Modell viel präziser beschreiben. Man müßte 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:
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);
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:
Java:
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:
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);
}

Die Objekte, die an diese Methoden übergeben werden, sind von einer eigenen "Event"-Klasse, die die Änderungen genauer beschreibt:
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!)
}


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


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

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.

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

So, kurze Pause und sacken lassen. Gleich geht's weiter! :)
 
Zuletzt bearbeitet von einem Moderator:

Marco13

Top Contributor
...und Teil 2... (25000 Zeichen Beschränkung und so...)

Wenn man diese Klasse nun als Listener zum Modell hinzufügt, werden während des "Spiels" Informationen über den Spielablauf auf der Konsole ausgegeben:
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);
    }
}


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:

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


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





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.

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


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:

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



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:

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


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.
 

ThreadPool

Bekanntes Mitglied
Das eigentliche Problem mit dem MVC war, dass die ursprünglichen Beiträge nur den Bezug zu Smalltalk hatten und durch die Versuche, das auf andere Bereiche zu übertragen, es dann auch zu unterschiedlichen Interpretation bzw. "Missverständnissen" kam.

Die folgenden Links würde ich in der Reihenfolge nach durchgehen. Danach sollte es klarer sein, wie was zusammenhängt und welche "Interpretationen" heute so verwendet werden etc.

Java SE Application Design With MVC
GUI Architectures
How to use Model-View-Controller (MVC)
Ctrl-Shift-B: Interactive Application Architecture Patterns
Java BluePrints - J2EE Patterns
http://www.ics.uci.edu/~redmiles/ics227-SQ04/papers/KrasnerPope88.pdf
Design Codes: Twisting the MVC Triad - Model View Presenter (MVP) Design Pattern

Weitere Beispiele:
Java: Model-View-Controller (MVC) Structure

Interessant ist vielleicht auch noch die etwas aktuellere "Vorstellung" des "Erfinders" des MVC. Der ursprüngliche Beitrag müsste auf seiner Seite auch als Scan zu finden sein.

http://heim.ifi.uio.no/~trygver/2003/javazone-jaoo/MVC_pattern.pdf
 
Zuletzt bearbeitet von einem Moderator:
Status
Nicht offen für weitere Antworten.

Neue Themen


Oben