design patterns


Hide Your Data

Establish Firm Contracts

Protect Your Constants

Control Your Own State

Delegate Responsibilities

Use Generic Interfaces

Delegate Control

patterns: Singleton, Observer, Factory, Decorator, Adapter

animation 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 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 regularly execute the tick() method of the animation code.

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 debugged.

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.
contents of file ClockObserver.java:


interface ClockObserver
   {
   /*
   ClockObservers must have a tick() method that the clock they're
   observing will ask them to execute 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 thread;
   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.
      */

      thread = new Thread(this);
      }

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

      thread = 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.
      */

      thread = 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.

      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 (thread == null)
         thread = new Thread(this);
      thread.start();
      }

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

   ///////////////////////////////////////////////////////////
   public void run()
      {
      while (thread != 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 execute 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.
      */
      }
   }


last | | contents | | next