B669: Personalized Data Mining and Mapping
Observers

 

Time for some working code. I write a lot of animation applets. these applets need at least two things: double-buffering and a thread. over and over again i'd write the same bits of code and only vary the actual animation code. eventually even i realized that this was stupid. what i needed was an observer.

the observer would be the animation code and it would observe a clock. the clock's sole purpose would be to execute the tick() method of the animation code once every n milliseconds. once i realized that, a whole subsection of the code simply went away. i only had to write Clock once and then i was done.

further, once i wrote Clock i saw ways to increase its functionality and modifying it was then very easy since everything was encapsulated there.

further, i realized that i could subtract the double-buffering out too. that went into a generic AnimationShell applet that my animation applets now subclass.

consequently, i can write a complex animation applet in less than a third the time it used to take me---and i can't make mistakes in the double-buffering or thread portions since they're already done and well debugged! this is what oo programming is all about.

study the code below carefully if these ideas are new to you. note that what was once 1 class now becomes 4 classes (the interface, the two helper classes, plus the guts of the animation code (not included below since it varies from application to application). but in exchange i've gained (1) robustness (this code is much easier to modify arbitrarily) (2) cleanliness (there is a clean separation of responsibilities amongst all the classes, each one has its job to do and ONLY that job) and (3) understandability (it should be very easy for anyone to figure out what i'm doing once they know about the observer pattern.

all 3 things together makes the code much more maintainable in case someone wants to change it in future. contents of file ClockObserver.java:


interface ClockObserver
	{
	/*
	ClockObservers must have a tick() method that the clock they're
	observing invokes on each clock tick.
	*/

	public void tick();
	}

contents of file Clock.java:

import java.lang.Runnable;
import java.lang.Thread;
import java.util.Vector;
import java.util.Enumeration;

class Clock
	implements Runnable
	{
	/*
	Define an object that regularly executes the tick() method of all
	its observers.

	Objects become observers by executing this clock's addClockObserver()
	method or by passing themselves to this clock on its creation.

	Public Constructors & Methods:
	------------------------------
	Clock()
	Clock(ticksPerSecond)
	Clock(ticksPerSecond, observer)
	addClockObserver(observer)
	deleteClockObserver(observer)
	start()
	stop()
	run()
	timeSinceLastTick()
	*/

	//my thread, observer list, clock rate, and temporary variables
	protected Thread ticker;
	protected Vector observers = new Vector();
	protected int ticksPerSecond = 25;
	protected long sleepTime = 40;
	protected long startTickTime= 0, currentTickTime= 0, lastTickTime= 0;
	protected long tickCount = 0;

	///////////////////////////////////////////////////////////////////////
	public Clock()
		{
		/*
		Create a clock with a default speed and no observers.
		*/

		ticker = new Thread(this);
		}

	///////////////////////////////////////////////////////////////////////
	public Clock(int ticksPerSecond)
		{
		/*
		Create a clock with a given speed and no observers.
		*/

		ticker = new Thread(this);
		this.ticksPerSecond = ticksPerSecond;
		//convert to milliseconds
		sleepTime = (long) (1000 / ticksPerSecond);
		}

	///////////////////////////////////////////////////////////////////////
	public Clock(int ticksPerSecond, ClockObserver observer)
		{
		/*
		Create a clock with a given speed and a given observer
		and start it.
		*/

		ticker = new Thread(this);
		this.ticksPerSecond = ticksPerSecond;
		//convert to milliseconds
		sleepTime = (long) (1000 / ticksPerSecond);
		addClockObserver(observer);
		this.start();
		}

	///////////////////////////////////////////////////////////////////////
	public synchronized void addClockObserver(ClockObserver observer)
		{
		observers.addElement(observer);
		}

	///////////////////////////////////////////////////////////////////////
	public synchronized void deleteClockObserver(ClockObserver observer)
		{
		observers.removeElement(observer);
		}

	///////////////////////////////////////////////////////////////////////
	public synchronized void die(ClockObserver observer)
		{
		/*
		Die if requested to by my last observer.

		Note: this would leave the observer (and any careless past observers)
		with a null pointer. how to fix that without requiring them to remember
		to null out their references to me? need some way to make myself private
		while still allowing arbitrary objects to observe me. I need a
		ClockManager.
		*/

		if ((observers.size() == 1) && observers.elementAt(0).equals(observer))
			{
			this.stop();
			observers = null;
			try {this.finalize();}
			catch (Throwable ignored) {} //just die
			}
		}

	///////////////////////////////////////////////////////////////////////
	public void start()
		{
		startTickTime = System.currentTimeMillis();
		lastTickTime = startTickTime;
		currentTickTime = startTickTime;

		if (ticker == null)
			ticker = new Thread(this);
		ticker.start();
		}

	///////////////////////////////////////////////////////////////////////
	public void stop()
		{
		ticker = null;
		}

	///////////////////////////////////////////////////////////////////////
	public void run()
		{
		while (ticker != null)
			{
			currentTickTime = System.currentTimeMillis();

			//execute all my observers' tick() methods
			Enumeration watchers = observers.elements();
			while (watchers.hasMoreElements())
				((ClockObserver) watchers.nextElement()).tick();
			tickCount++;

			long elapsedTime = System.currentTimeMillis() - currentTickTime;

			lastTickTime = currentTickTime;

			//the time to sleep depends on how much time it took to call all
			//the tick() methods, so sleep only long enough to keep the beat
			if (elapsedTime < sleepTime)
				{
				try {Thread.sleep(sleepTime - elapsedTime);}
				catch (InterruptedException exception)
					{
					System.out.println("Clock: something bad happened.");
					exception.printStackTrace();
					throw new RuntimeException(exception.toString());
					}
				}
			}
		}

	///////////////////////////////////////////////////////////////////////
	public long timeSinceLastTick()
		{
		return currentTickTime - lastTickTime;
		}

	///////////////////////////////////////////////////////////////////////
	protected long getTickCount()
		{
		return tickCount;
		}
	}

contents of file AnimationShell.java:

import ClockObserver;
import Clock;

import java.applet.Applet;
import java.awt.Image;
import java.awt.Graphics;
import java.awt.Color;

public class AnimationShell extends Applet
	implements ClockObserver
	{
	/*
	This applet shell can be subclassed to produce animation applets.
	It uses an offscreen image to double buffer the animation and so
	avoid image flicker and it observes a clock that executes its tick()
	method to control when the animation is redrawn. It also lets subclasses
	control how the animation is drawn (whether it's to be paused, resumed,
	restarted, or killed).

	Public Methods:
	---------------
	init()
	start()
	stop()
	run()
	update(graphicsContext)
	paint(graphicsContext)
	tick()
	pauseAnimation()
	resumeAnimation()
	restartAnimation()
	endAnimation()
	*/

	//my offscreen bitmap and its graphics context
	protected Image offscreenImage;
	protected Graphics offscreenGraphics;

	//my clock and its speed
	Clock clock;
	protected int CLOCK_TICKS_PER_SECOND = 25;

	//my window's background color and dimensions
	protected Color backgroundColor = Color.black;
	protected int windowWidth, windowHeight;

	//the animation controller
	protected boolean paused;

	////////////////////////////////////////////////////////////////////
	public void init()
		{
		/*
		Setup my onscreen and offscreen bitmaps.
		*/

		//fetch my window's dimensions and blank the screen
		windowWidth = this.getSize().width;
		windowHeight = this.getSize().height;
		this.setBackground(backgroundColor);

		//setup my offscreen bitmap
		offscreenImage = this.createImage(windowWidth, windowHeight);
		offscreenGraphics = offscreenImage.getGraphics();
		offscreenGraphics.setColor(backgroundColor);
		offscreenGraphics.fillRect(0, 0, windowWidth, windowHeight);

		//start my clock
		clock = new Clock(CLOCK_TICKS_PER_SECOND, this);

		//start the animation
		this.start();
		}

	////////////////////////////////////////////////////////////////////
	public void start()
		{
		/*
		Start the animation.
		*/

		paused = false;
		}

	////////////////////////////////////////////////////////////////////
	public void stop()
		{
		/*
		Stop the animation.
		*/

		paused = true;
		}

	////////////////////////////////////////////////////////////////////
	public void tick()
		{
		/*
		This method is regularly executed by my clock.
		*/

		if (! paused)
			run();
		}

	////////////////////////////////////////////////////////////////////
	public void run()
		{
		/*
		Generate the next animation frame and paint it.
		*/

		updateAnimation();
		this.repaint();
		}

	////////////////////////////////////////////////////////////////////
	public void pauseAnimation()
		{
		this.stop();
		}

	////////////////////////////////////////////////////////////////////
	public void resumeAnimation()
		{
		this.start();
		}

	////////////////////////////////////////////////////////////////////
	public void restartAnimation()
		{
		/*
		Start the animation over from the beginning.
		*/

		this.stop();
		this.init();
		}

	////////////////////////////////////////////////////////////////////
	public void endAnimation()
		{
		/*
		Kill the animation dead.
		*/

		this.stop();
		offscreenImage = null;
		offscreenGraphics = null;
		clock.die(this);
		clock = null;
		}

	////////////////////////////////////////////////////////////////////
	public void update(Graphics graphicsContext)
		{
		/*
		Overridde the default Applet.update() to decrease flicker.
		*/

		paint(graphicsContext);
		}

	////////////////////////////////////////////////////////////////////
	public void paint(Graphics graphicsContext)
		{
		/*
		Paint my offscreen bitmap to my window.
		*/

		//blank my offscreen bitmap
		offscreenGraphics.setColor(backgroundColor);
		offscreenGraphics.fillRect(0, 0, windowWidth, windowHeight);

		//paint the current animation frame into it
		paintAnimationFrame(offscreenGraphics);

		//draw it to the screen
		graphicsContext.drawImage(offscreenImage, 0, 0, this);
		}

	////////////////////////////////////////////////////////////////////
	protected void updateAnimation()
		{
		/*
		Update the animation that i will later paint to my offscreen bitmap.

		This method does nothing here; it's to be overridden by subclasses.
		*/
		}

	////////////////////////////////////////////////////////////////////
	protected void paintAnimationFrame(Graphics graphicsContext)
		{
		/*
		Paint a frame of the animation into graphicsContext.

		This method does nothing here; it's to be overridden by subclasses.
		*/
		}
	}