Design-Fragen

Inhalt

 

 

Ein Projekt

 

Das Design

 

Das Brett

 

Grafik-Ausgabe

 

Das Statistik-Panel

 

Der Controller

 

Das Applet

 

Maus-Eingaben

 

Design-Kritik

Ein Projekt

Wir wollen in diesem Abschnitt prinzipielle Fragen des Designs von Java-Applikationen an einem Beispiel studieren. Gleich vorweg muss jedoch gesagt werden, dass Fragen der Programmentwicklung sich in sehr vielfältiger Form stellen. Dieser Abschnitt kann nur einen ersten Leitfaden geben. Auch wird nicht jeder Java-Experte die gleichen Ideen über gutes Design vertreten. Es gibt jedoch einige Übereinstimmungen, die wir hier demonstrieren wollen. Diese Grundkonzepte werden auch von Sun selbst beim Design von Java-Beans und in neueren Entwicklungen der API berücksichtigt.

Als Projekt wählen wir eine Simulation des Life-Spiels von Conway. Es handelt sich dabei um einen zellulären Automaten, der auf einem Gitter Generation nach Generation von Völkern nach einfachen Regeln erzeugt. Wir wählen die einfachsten Regeln, die folgendermaßen aussehen.

Wir werden ein Applet erzeugen, das ein Life-Spiel graphisch ablaufen lässt und Statistiken über den Ablauf anzeigt.

Das fertige Applet läuft nur mit einem Browser, der Java 1.1 versteht (Internet Explorer 4, Netscape 4 mit 1.1 Preview oder Netscape 4.06). Sie können es hier zunächst ausprobieren.

Das Design

Wichtig ist, dass wir die einzelnen Teile, aus denen das Programm besteht, voneinander isolieren. In unserem Fall kann man sofort mindestens fünf Teile erkennen.

Es ist nun wichtig, dass man diese Teile auch als separate Objekte entwickelt. Dies fördert zum einen die Klarheit des Codes und damit die Wartbarkeit des ganzen Programms, und es dient zum anderen der Wiederverwendbarkeit der Teile. Betrachtet man etwa die Trennung von Datenmodell (das Brett) und seiner Darstellung (das Panel), so ist klar, dass man damit die Möglichkeit hat, das Brett auf verschiedene Arten darzustellen. Außerdem kann man für theoretische Zwecke Bretter erzeugen, die überhaupt nicht dargestellt werden.

Wichtig ist außerdem die Kommunikation zwischen den Teilen. Wir verwenden dazu natürlich Methodenaufrufe der Objekte, aber auch denselben EventListener-Mechanismus, den Java verwendet.

Das Brett

Als erstes entwickeln wir das Brett und testen es. Die Datenstruktur besteht einfach aus einem Array. Dem Brett wird bei der Initialisierung die Größe übergeben. Es hat Methoden zum Füllen mit einer zufälligen Zahl von lebendigen Feldern, zum Ablesen und Setzen einzelner Felder und zum Auslesen der Gesamtzahl der Individuen.

Das Brett enthält keinen Code zur Darstellung. Dies wird von anderen Klassen übernommen. Übrigens ist der erste Kommentar dazu gedacht, von javadoc (einen Programm, das automatisch Dokumentation erzeugt) ausgenutzt zu werden.

/**
<P>
This class contains a game of life board. This
basic class playes on a NxM board with connected
boundaries (a torus) and uses the following rules.
<UL>
<LI> death at 0-1 and 4 neighbors,
<LI> new life at 3 neighbors,
</UL>
though a child class may override the generate
method.
*/

public class Board
{	
	// the board and its size.
	
	protected boolean B[][],Back[][];
	protected int N,M,Count;
	
	public Board (int n, int m)
	{	N=n; M=m;
		B=new boolean[N][M];
		Back=new boolean[N][M];
		Count=0;
	}
	
	public Board (int n)
	{	this(n,n);
	}
	
	protected boolean generate (int i, int j)
	// the cellular rule, override if you like
	{	int neighbors=0;
		if (B[(i+N-1)%N][j]) neighbors++;
		if (B[(i+1)%N][j]) neighbors++;
		if (B[i][(j+M-1)%M]) neighbors++;
		if (B[i][(j+1)%M]) neighbors++;
		if (B[(i+N-1)%N][(j+M-1)%M]) neighbors++;
		if (B[(i+1)%N][(j+1)%M]) neighbors++;
		if (B[(i+1)%N][(j+M-1)%M]) neighbors++;
		if (B[(i+N-1)%N][(j+1)%M]) neighbors++;
		switch (neighbors)
		{	case 2 : return B[i][j];
			case 3 : return true;
		}
		return false;
	}
	
	// *********** to be called from outside ************
	
	synchronized public void clear ()
	// clear all fields
	{	for (int i=0; i<N; i++)
			for (int j=0; j<M; j++)
				B[i][j]=false;
		Count=0;
	}
	
	synchronized public void set (double random)
	// fill with random degree
	{	for (int i=0; i<N; i++)
			for (int j=0; j<M; j++)
			{	B[i][j]=Math.random()<random;
				if (B[i][j]) Count++;
			}
	}
	
	synchronized public void set (int i, int j)
	// set a field
	{	if (!B[i][j]) Count++;
		B[i][j]=true;
	}

	synchronized public void clear (int i, int j)
	// clear a field
	{	if (B[i][j]) Count--;
		B[i][j]=false;
	}
	
	public boolean get (int i, int j)
	// get content of a field
	{	return B[i][j];
	}
	
	synchronized public void generate ()
	// generate next generation
	{	for (int i=0; i<N; i++)
			for (int j=0; j<M; j++)
				Back[i][j]=generate(i,j);
		Count=0;
		for (int i=0; i<N; i++)
			for (int j=0; j<M; j++)
			{	B[i][j]=Back[i][j];
				if (B[i][j]) Count++;
			}
	}
	
	public int getWidth () { return M; }
	public int getHeight () { return N; }
	public int getCount () { return Count; }
}

Man beachte, dass dieser Code ziemlich einfach ist und daher sicher nicht die schnellstmögliche Implementation des Generierungsalgorithmus. Es ist jedoch ohne Kenntnis von Details der verwendeten virtuellen Maschine nicht klar, wie sich eine gute Beschleunigung erreichen lässt. Auf eine Maschine mit mehreren Prozessoren könnte man etwa das Update auf mehrere Threads verteilen. Es mag auch günstiger sein, ein eindimensionales Array oder sogar ein Bit-Feld zu verwenden.

Die wichtigste Methode ist generate, die eine neue Generation erzeugt. Bei anderen Regeln kann man einfach die Methode generate(i,j) überlagern.

Manche Methoden sind synchronisiert. Dies ist erst später notwendig, wenn der Benutzer das Brett selbst verändern kann. Die Klasse müsste sich sonst auf eine externe Synchronisation zwischen Generationserzeugung und Benutzereingriffen verlassen. Im Übrigen ist es in diesem Fall vielleicht nicht so tragisch, wenn ein inhomogener Zustand entsteht.

Man kann auch schon einen ersten Test ablaufen lassen, indem man eine einfache Ausgabe auf der Konsole realisiert.

public class Test
{	public static void main (String args[])
	{	Board b=new Board(10,12);
		b.set(0.3);
		while (b.getCount()>4)
		{	for (int i=0; i<b.getHeight(); i++)
			{	for (int j=0; j<b.getWidth(); j++)
					if (b.get(i,j)) System.out.print("O");
					else System.out.print(".");
				System.out.println();
			}
			System.out.println("Count "+b.getCount());
			System.out.println();
			b.generate();
		}
	}
}

Dieser Code genügt vollkommen. Die Ausgabe lässt sich mit

java Test >output

in eine Datei umlenken, wo man sofort die Korrektheit verifizieren kann.

Grafik-Ausgabe

Als nächstes realisieren wir die Grafikausgabe in einem Panel. Die Ausgaberoutine ist recht einfach. Wir entscheiden uns für ein Panel, das mit Kästchen gefüllt wird, die den Status der Felder durch Farben anzeigen. Über das ganze wird ein Gitter gezeichnet.

Die Frage ist, wie man die Aktualisierung der Ausgabe anstößt. Letztendlich wird das Erzeugen der nächsten Generation natürlich durch die zentrale Steuerung erreicht. Dazu könnte die zentrale Steuerung die Ausgabe-Methode der Ausgabe-Klasse einfach aufrufen. Allerdings ist es viel natürlicher und auch flexibler, wenn das Board die Ausgabe selbst neu anstößt, sobald dies notwendig ist.

Dazu muss es eine Version von Board geben, die alle Stellen, die von einer Änderung wissen wollen dann informiert, wenn dies notwendig ist. Wir implementieren daher ein Listener-Interface, das von interessierten Objekten implementiert werden muss. Außerdem leiten wir die Board-Klasse dahingehend ab, dass sich bei ihr interessierte Klassen registrieren lassen. Diese Aufgabe wird dadurch erschwert, dass wir mehrere Listener zulassen wollen, die in einem Vector gespeichert werden müssen.

Übrigens werden wir später dieses Design wieder ändern. Dies zeigt, dass der erste Einfall nicht immer der richtigste ist. Man sollte sich zu keinem Zeitpunkt scheuen, ein Design vollständig umzuwerfen, wenn man überzeugt ist, dass dies notwendig ist.

Wir wählen außerdem noch einen flexibleren Zugang, indem wir der Methode hasChanged des Listeners ein Event übergeben, das das Ereignis genauer beschreibt. Dies wäre nicht nötig. Stattdessen könnte es mehrere Methoden geben, die unterscheiden, ob sich nur ein einzelnes Feld geändert hat oder ob eine neue Generation gezüchtet wurde. Wir wollen hier aber den allgemeinsten Zugang verwenden. Ein solches Event ist im Prinzip irgendein Objekt. Wir leiten aber diese Event-Klasse von EventObject ab.

Zunächst also schreiben wir die gewünschte Erweiterung der Board-Klasse.

import java.util.*;

/**
<P>
A board that can notify BoardChangeListener-Objects of
board changes.
<P>
*/

public class ChangingBoard extends Board
{	
	// board constructors
	
	public ChangingBoard (int n, int m)
	{	super(n,m);
	}
	public ChangingBoard (int n)
	{	super(n);
	}
	
	// override the changes to notify listeners
	
	public void clear ()
	{	super.clear();
		notify(new BoardChangeEvent(this,
			BoardChangeEvent.ALL));
	}
	public void clear (int n, int m)
	{	super.clear(n,m);
		notify(new BoardChangeEvent(this,
			BoardChangeEvent.SINGLE,n,m));
	}
	public void set (int n, int m)
	{	super.set(n,m);
		notify(new BoardChangeEvent(this,
			BoardChangeEvent.SINGLE,n,m));
	}
	public void set (double f)
	{	super.set(f);
		notify(new BoardChangeEvent(this,
			BoardChangeEvent.ALL));
	}
	public void generate ()
	{	super.generate();
		notify(new BoardChangeEvent(this,
			BoardChangeEvent.GENERATION));
	}
	
	// the listeners Vector and methods for it
	
	Vector listeners=new Vector();
	
	void addBoardChangeListener (BoardChangeListener l)
	{	listeners.addElement(l);
	}
	void removeBoardChangeListener (BoardChangeListener l)
	{	listeners.removeElement(l);
	}
	
	public void notify (BoardChangeEvent e)
	{	for (int i=0; i<listeners.size(); i++)
			((BoardChangeListener)
				listeners.elementAt(i)).hasChanged(e);
	}
}

Die Listener werden also in einem Vektor gespeichert, wobei wir einfach die in Java vorhandende Klasse Vector verwenden. Die Methode notify benachrichtigt alle Listener in diesem Vektor. Als Belohnung für die Verwendung eines Event-Obkjektes müssen wir diese Schleife nur einmal schreiben.

Übrigens gibt es hier ein Problem, das darin besteht, dass während der Verarbeitung eines Events ein Listener den Vektor verändert. Man sollte daher eigentlich den Vektor vor der Schleife kopieren und die Kopie verwenden. Wir haben hier darauf verzichtet.

Wir benütigen hier noch die Klassen BoardChangeEvent und BoardChangeListener. Ersteres leiten wir von EventObject ab.

import java.util.*;

/**
<P>
This class is the parameter of hasChanged in the 
BoardChangeListener interface.
<P>
*/

public class BoardChangeEvent extends EventObject
{	public static final 
		int SINGLE=0, // a single stone has changed
		ALL=1, // all stones have changed
		GENERATION=2; // a new generation was breeded
	protected int Id;
	protected int N,M; // which stone has changed?
	
	public BoardChangeEvent (Object source, int id)
	{	super(source);
		Id=id;
	}

	public BoardChangeEvent (Object source, int id, int n, int m)
	{	super(source);
		Id=id; N=n; M=m;
	}
	
	public int getId () { return Id; }
	public int getN () { return N; }
	public int getM () { return M; }
}

Danach entwerfen wir den Listener.

public interface BoardChangeListener
{	public void hasChanged (BoardChangeEvent e);
}

Wie man sieht, ist dies verhältnismäßig viel Schreibarbeit, die sich aber in einem klaren Design niederschlägt. Für ein einfaches Projekt wäre es natürlich möglich, auf die Definition eines Events zu verzichten. Weiter vereinfacht sich der Code, wenn man nur einen einzigen Listener zulässt.

Das Hauptprogramm zum Testen des bisher Erarbeiteten ändert sich nun folgenermaßen.

public class Test 
	implements BoardChangeListener
{	public static void main (String args[])
	{	ChangingBoard b=new ChangingBoard(10,12);
		b.addBoardChangeListener(new Test());
		b.set(0.3);
		while (b.getCount()>4) b.generate();
	}
	
	public void hasChanged (BoardChangeEvent e)
	{	if (e.getId()==BoardChangeEvent.GENERATION)
		{	Board b=(Board)e.getSource();
			for (int i=0; i<b.getHeight(); i++)
			{	for (int j=0; j<b.getWidth(); j++)
					if (b.get(i,j)) System.out.print("O");
					else System.out.print(".");
				System.out.println();
			}
			System.out.println("Count "+b.count());
			System.out.println();
		}
	}
}

Die eigentliche Ausgabe geschieht nun ein einem Panel, das das Interface BoardChangeListener implementiert.

import java.awt.*;
import java.awt.event.*;

/**
<P>
The board panel, which really displays the board.
Uses a background image with a fixed size.
<P>
Note that the Image may only be produced by panels
which already display correctly.
<P>
We also allow at most one BoardMouseListener to
listen to mouse presses on the board.
*/

public class BoardPanel extends Panel
	implements BoardChangeListener
{	ChangingBoard B;
		// the board to be observed and displayed

	// the image and its handle
	Image I=null;
	Graphics Ig=null;
	
	// precomputed colors
	Color
		alive=Color.red.darker().darker(),
		dead=Color.gray.brighter();
	
	// we use a fixed cell size here
	int Size=10;
	
	public BoardPanel (ChangingBoard b)
	// intialize and set add self as listener to the board
	{	B=b;
		B.addBoardChangeListener(this);
	}
	
	public void paint (Graphics g)
	// paint, but create image on first call
	{	if (I==null) makeImage();
		Dimension d=getSize();
		g.drawImage(I,d.width/2-I.getWidth(this)/2,
			d.height/2-I.getHeight(this)/2,this);
	}
	
	public void makeImage ()
	// create image and its graphic handle
	{	I=createImage(Size*B.getWidth()+1,Size*B.getHeight()+1);
		Ig=I.getGraphics();
	}
	
	public void paintImage ()
	// paint the image with the board, but
	// create on first call
	{	if (I==null)
		{	makeImage();
		}
		
		int n=B.getHeight(),m=B.getWidth(); // board dimensions
		int w=Size; // use the fixed size here
		int i,j,col,row;
		
		// draw all cells
		row=0;
		for (i=0; i<n; i++)
		{	col=0;
			for (j=0; j<m; j++)
			{	if (B.get(i,j)) Ig.setColor(alive);
				else Ig.setColor(dead);
				Ig.fillRect(col,row,w,w);
				col+=w;
			}
			row+=w;
		}
		
		// draw vertical and horizontal lines
		Ig.setColor(Color.black);
		row=0;
		for (i=0; i<=n; i++)
		{	Ig.drawLine(0,row,m*w,row);
			row+=w;
		}
		col=0;
		for (j=0; j<=m; j++)
		{	Ig.drawLine(col,0,col,n*w);
			col+=w;
		}
	}
	
	public void hasChanged (BoardChangeEvent e)
	// called by the board
	{	paintImage();
		Graphics g=getGraphics();
		paint(g);
		g.dispose();
	}
	
	// override getPreferredSize for container
	public Dimension getPreferredSize ()
	{	return new Dimension(Size*B.getWidth()+1,
			Size*B.getHeight()+1);
	}
	public Dimension getMinimumSize ()
	{	return getPreferredSize();
	}
	
}

Dies ist eine einfache gepufferte Ausgabe des Brettes. Das Panel registriert sich selber als Listener beim Brett. Das Speicher-Image wird erst dann erzeugt, wenn das Panel auf dem Bildschirm neu gezeichnet werden muss. Die Methode createImage von Komponenten funktioniert nämlich erst, wenn die Komponente auf dem Bildschirm initialisiert ist.

Wir geben noch die gewünschte Größe mir preferredSize zurück, indem wir die entsprechenden Methoden der Component-Klasse überschreiben. Diese Methode wird von Layout-Managern aufgerufen, um der Komponente genügend Platz zu verschaffen.

Um die Ausgabe wirklich genießen zu können, muss man nun ein erweitertes Testprogramm schreiben, dass das Panel in einen Frame integriert. Außerdem sollte man eine Verzögerungsschleife implementieren, damit man die einzelnen Zustände sehen kann.

import java.awt.*;

class TestFrame extends Frame
	implements Runnable
{	ChangingBoard B;
	
	public TestFrame ()
	{	super("Test Frame");
		setLayout(new BorderLayout());
		setSize(200,200);
		B=new ChangingBoard(10,12);
		BoardPanel p=new BoardPanel(B);
		add("Center",p);
		show();
		new Thread(this).start();
	}

	public void run ()
	{	B.set(0.3);
		while (true)
		{	try
			{ Thread.currentThread().sleep(1000); } 	
			catch (Exception e) {}
			B.generate();
		}
	}
}

public class Test
{	public static void main (String args[])
	{	new TestFrame();
	}
}

Dies ist schon ein etwas komplexeres Hauptprogramm. Es verwendet einen Thread, um die Generationen neu zu erzeugen, der mit einer Warteanweisung von einer Sekunde verzögert wird.

Das Statistik-Panel

Aufgrund unseres allgemeinen Ansatzes ist es leicht möglich, ein weiteres Panel einzufügen, dass auf Board-Änderungen reagiert. Dieses Panel zeichnet eine Kurve, die angibt, wie stark das Brett mit Leben gefüllt ist.

import java.awt.*;
import java.util.*;

/**
<P>
This BoardChangeListener shows the count of alive members
in graphical line.
<P>
*/

public class StatisticsPanel extends Panel
	implements BoardChangeListener
{	protected ChangingBoard B; // the board to listen to

	protected Vector V; // the vector of counts
	
	protected Color blue=Color.blue.darker();
	
	public StatisticsPanel (ChangingBoard b)
	{	B=b;
		V=new Vector();
		B.addBoardChangeListener(this);
			// register yourself
	}
	
	int wOld,hOld; 
		// last plot position for the continuous line plot
	
	synchronized public void paint (Graphics g)
	// replot everything
	{	Dimension d=getSize();
		// draw a black frame
		g.setColor(Color.black);
		g.drawRect(0,0,d.width-1,d.height-1);
		// draw the blue statistics
		int t=B.getWidth()*B.getHeight();
			// total number of fields
		g.setColor(blue);
		int hold=0,wold=0;
		for (int i=0; i<V.size(); i++)
		{	int n=((Integer)V.elementAt(i)).intValue();
			int h=d.height-1-(d.height-2)*n/t;
			if (i>0) g.drawLine(wold,hold,i+1,h);
			wold=i+1; hold=h; // remember last position
			if (i>d.width-1) break;
		}
		hOld=hold; wOld=wold;
	}
	
	
	synchronized public void hasChanged (BoardChangeEvent e)
	// draw a new line, if a new generation has been generated,
	// else clear the plot.
	{	if (e.getId()==BoardChangeEvent.GENERATION)
		{	V.addElement(new Integer(B.getCount()));
			Graphics g=getGraphics();
			g.setColor(blue);
			Dimension d=getSize();
			int w=V.size(),
				h=d.height-1-(d.height-2)*B.getCount()/
					(B.getWidth()*B.getHeight());
			if (w>1 && w<d.width-1) g.drawLine(wOld,hOld,w,h);
			wOld=w; hOld=h;
		}
		else
		{	int n=V.size();
			V=new Vector();
			if (n>0) repaint();
		}
	}
}

Hier ist die wesentliche Schwierigkeit, die Ausgabe nicht jedesmal neu Zeichnen zu müssen. Wir besorgen uns also ein Graphics-Object für das Panel und zeichnen nur die Fortsetzung der Statistik bei einer neuen Generation. Falls das Board auf andere Weise geändert wurde, löschen wir die Statistik ganz.

Der Controller

Es fragt sich nun, wie man das ganze Applet und den Ablauf der Generationserzeugung steuert. Dazu dient die folgende Klasse.

/**
<P>
Controls the board with a running thread.
The actions of this thread are determined by
a variable and may cause the board to breed
a new generation, reset itself, or simply
do nothing. Singe steps are also possible.
<P>
*/

public class BoardControl
	implements Runnable
{	protected Board B; // the board to be controlled
	protected Thread T; // the running thread
	public int Delay=200; // a delay paramter in milliseconds
	public boolean Stopped;
		// this will completely stop the thread
		// the control will need a new init to start running
		// again.
	private int Action; // the next action to be performed
	private final int GENERATION=0,RESET=1,STOP=2,STEP=3,
			CONTINUE=4,CLEAR=5;

	public BoardControl (Board b)
	{	B=b;
	}
	
	public void init ()
	// get the thread running, initialize the board
	{	B.set(0.5);
		Thread T=new Thread(this);
		Stopped=false;
		T.start();
	}
	
	public void stop ()
	// stop the thread, need init to start again
	{	Stopped=true;
	}
	
	// methods to control the board from a button panel or so
	public void actionStop () { Action=STOP; }
	public void actionStep () { Action=STEP; }
	public void actionContinue () { Action=CONTINUE; }
	public void actionReset () { Action=RESET; }
	public void actionClear () { Action=CLEAR; }
	
	public void run ()
	// the running method of the thread
	{	while (!Stopped)
		{	try { T.sleep(Delay); } catch (Exception e) {}
			switch (Action)
			{	case RESET :
					B.set(0.5);
					Action=GENERATION;
					break;
				case STOP :
					break;
				case GENERATION :
					B.generate();
					break;
				case STEP :
					B.generate();
					Action=STOP;
					break;
				case CONTINUE :
					B.generate();
					Action=GENERATION;
					break;
				case CLEAR :
					B.clear();
					Action=STOP;
					break;
			}
		}
	}
	
}

Diese Klasse enthält einen Thread, der das Board ständig nach den Vorgaben erneuert. Welche Aktion im nächsten Zeittakt zu erledigen ist, wird durch die Variable Action gesteuert. Es wird also nicht der Thread bei einer Benutzeraktion unterbrochen, sondern nach Ablauf seiner Wartezeit wird eine Variable daraufhin getestet, was als nächstes zu tun ist. Dieser Ansatz verhilft zu einem klaren Design, ist aber nicht bei allen Threads als Steuerung anwendbar.

Das Applet

Wir sind nun soweit, dass wir das Applet zu unserer Anwendung schreiben können.

import java.awt.*;
import java.awt.event.*;
import java.applet.*;
import java.util.*;

/**
<P>
This applet demonstrates the game of life in
a Web applet. The user can press buttons to
control the iteration or reset it.
<P>
Main purpose is the demonstration of a
Java design.
<P>
*/

public class LifeApplet extends Applet
	implements ActionListener
{	BoardControl BC;
		// the board control object has methods to
		// reset the board, stop the iteration etc.

	/*
	Set up the Applet design
	**/

	public LifeApplet ()
	{	setLayout(new BorderLayout());

		// generate the board
		ChangingBoard b=new ChangingBoard(15,15);
		
		// generate the center panel for the board
		// and the statistics
		Panel cp=new Panel();
		cp.setLayout(new GridLayout(1,0));
		// add a board panel
		BoardPanel BP=new BoardPanel(b);
		cp.add(BP);
		// add a statistics panel
		Panel pstat=new Panel();
		pstat.setLayout(new BorderLayout());
		pstat.add("North",new Label(string("LifeCount")));
		pstat.add("Center",new StatisticsPanel(b));
		cp.add(pstat);
		add("Center",new Panel3D(cp));
		
		// generate the south panel with th buttons
		BC=new BoardControl(b); // the controler object
		Panel sp=new Panel();
		Button button;
		sp.add(button=new Button(string("Reset")));
		button.addActionListener(this);
		sp.add(button=new Button(string("Stop")));
		button.addActionListener(this);
		sp.add(button=new Button(string("Step")));
		button.addActionListener(this);
		sp.add(button=new Button(string("Continue")));
		button.addActionListener(this);
		sp.add(button=new Button(string("Clear")));
		button.addActionListener(this);
		add("South",new Panel3D(sp));
	}
	
	public void actionPerformed (ActionEvent e)
	// react on button presses
	{	if (e.getActionCommand().equals(string("Reset")))
			BC.actionReset();
		else if (e.getActionCommand().equals(string("Stop")))
			BC.actionStop();
		else if (e.getActionCommand().equals(string("Step"))) 
			BC.actionStep();
		else if (e.getActionCommand().equals(string("Continue")))
			BC.actionContinue();
		else if (e.getActionCommand().equals(string("Clear")))
			BC.actionClear();
	}
	
	public void start ()
	// start applet, i.e. initialize board control
	// may be called several times
	{	BC.init();
	}

	public void stop ()
	// stop the applet, i.e. stop control thread
	// called when the user leave page
	{	BC.stop();
	}

	// things for the internationalization of the applet
	
	static protected ResourceBundle Resource;
	
	static // load the resource once
	{	try
		{	Resource = ResourceBundle.getBundle("LifeResources");
		}
		catch (Exception e)
		{	System.out.println(e);
			Resource=null;
		}
	}
	
	static String string (String item)
	// translate a resource string
	{	try
		{	return Resource.getString(item);
		}
		catch (Exception e)
		{	return item;
		}
	}		
}

Der Aufbau des Applets benutzt ineinander geschachtelte Panele. Im Zentralpanel ist das Board und ein Panel mit der Statistik und einem Label enthalten, und das Südpanel enthält die Steuerungsknöpfe.

Die beiden Hauptpanele haben wir in ein Panel3D eingelagert, um die Optik zu verbessern. Panel3D nimmt eine Komponente auf und zeichnet sie mit einem Rahmen, der ein erhabenes Rechteck darstellt. Dazu verwenden wir einen eigenen Layout-Manager, indem wir einfach doLayout überschreiben.

import java.awt.*;

/**
Panel3D extends the Panel class with a 3D look.
*/

public class Panel3D extends Panel
{	Component C;
	
	/**
	Adds the component to the panel.
	This component is resized to leave 5 pixel on each side.
	*/
	public Panel3D (Component c)
	{	C=c;
		add(C);
	}
	
	/**
	With this constructor, you take care of the boundary
	space yourself.
	*/
	public Panel3D ()
	{	C=null;
	}
	
	public void paint (Graphics g)
	{	g.setColor(this.getBackground());
		g.fill3DRect(0,0,getSize().width-1,getSize().height-1,true);
	}
	public void update (Graphics g)
	{	paint(g);
	}
	
	public void doLayout ()
	{	if (C!=null)
		{	C.setLocation(5,5);
			C.setSize(getSize().width-10,getSize().height-10);
			C.doLayout();
		}
		else super.doLayout();
	}
}

Wie man sieht, benutzen wir gleich noch eine Resourcen-Datei, die wir in Deutsch und Englisch mitliefern. Die deutsche Datei sieht so aus.

Stop=Anhalten
Step=Einzelschritt
Continue=Weiter
Reset=Neu Starten
LifeCount=Zähler
Clear=Löschen

Man beachte, dass die Namen links vom Gleichheitszeichen keine Leerzeichen enthalten dürfen.

Maus-Eingaben

Schließlich implementieren wir noch die Mögichkeit, dass der Benutzer das Brett durch Mausklicks ändern kann. Das einzige Objekt, das sinnvoll auf diese Mausklicks reagieren kann, ist das Board-Panel. Dieses Panel benachrichtigt nun unseren Controller, der die eigentlichen Änderungen vornimmt. Dazu implementiert der Controller das Interface BoardMouseListener.

public interface BoardMouseListener
{	public void mouseClicked (int i, int j);
}

Wir betreiben nun nicht den zusätzlichen Aufwand, eine Ereignisklasse abzuleiten. Auch lassen wir nur einen Listener zu, wie man an den Änderung in BoardPanel erkennt.

public class BoardPanel extends Panel
	implements BoardChangeListener, MouseListener
{	...
	
	public BoardPanel (ChangingBoard b)
	// intialize and set add self as listener to the board
	{	...
		addMouseListener(this);
	}
	
	...

	// react on mouse clicks

	private BoardMouseListener BML=null;
		// only one listener this time
	
	public void setBoardMouseListener (BoardMouseListener bml)
	{	BML=bml;
	}
	
	public void mousePressed (MouseEvent e) {}	
	public void mouseReleased (MouseEvent e) {}	
	public void mouseClicked (MouseEvent e)
	// check position and call the BoardMouseListener
	{	if (BML==null) return;
		Dimension d=getSize();
		int n=(e.getX()-(d.width/2-I.getWidth(this)/2))/Size,
			m=(e.getY()-(d.height/2-I.getHeight(this)/2))/Size;
		if (n<0 || n>=B.getWidth() || m<0 || m>=B.getHeight()) 
			return;
		BML.mouseClicked(m,n);
	}	
	public void mouseEntered (MouseEvent e) {}	
	public void mouseExited (MouseEvent e) {}	
}

Schließlich muss man noch im Applet den Controller beim Board-Panel registrieren und im Controller den Code zur Verarbeitung des Ereignisses implementieren.

Design-Kritik

Wir sollten uns nach der Erstellung des Programmes nicht nur fragen, ob das Programm korrekt funktioniert. Vielmehr ist es wichtig zu fragen, ob das Design auch erweiterten Ansprüchen standhalten würde. Dabei ist ein wenig Weitsicht erforderlich. Man denke sich dazu einfach ein paar Dinge aus, um die man die gerade erstellte Applikation erweitern könnte.

Denkbar wäre in unserem Fall etwa, dass der Benutzer eine andere Regel für die Erzeugung einer Generation auswählen kann. Da unsere Regel als Methode von Board implementiert ist, ist es am natürlichsten, Board mit einer neuen Methode zu überlagern. Da wir aber schon ChangingBoard von Board abgeleitet haben, sind wir in dieser Beziehung in einem Dilemma. Es wäre in der Tat natürlicher, ChangingBoard das zu überwachende Board als Parameter zu übergeben. Dann ließe sich das Board auch sehr leicht auswechseln. Ma sollte sich nun nicht scheuen, eine solche Design-Änderung auch tatsächlich durchzuführen. Die Stärke des alten Designs zeigt sich dann, wenn eine solche Umwälzung leicht machbar ist.

Weiter wäre denkbar, dass der Beutzer zwischen verschiedenen Arten, das Brett auszugeben umschalten kann. Dies könnte man dadurch realisieren, dass man das vorhandene Board-Panel in einem Applet durch eine Kindklasse ersetzt. Dies ist an sich nicht schwer zu realisieren. Es gibt dazu die Methode remove, die eine Komponente entfernt. Wichtig ist jedoch, dass die entfernte Komponente nicht and das Brett gebunden ist. Dazu muss man sie lediglich als BoardChangeListener entfernen. Hier also haben wir eine Stärke unseres Designs.

Zurück zum Java-Kurs