Recall the example of hotel design given in the first chapter.
With inheritance and interfaces you can build a lot of complex cross-class
structure, but that isn't enough if the problem you're trying to solve
is sufficiently interesting.
For example, a Guest
has accidentally started a fire
and calls down to the FrontDesk
to report the problem.
The FrontDesk
has standing instructions
to call the Manager
in case of fire.
Now suppose that during design you had decided that
every time there is a potential for fire,
the relevant class has to call the Manager
class'
handleFire()
method.
Since nearly any staff person object could notice a fire,
nearly every staff person class would call Manager
in case of fire.
This works fine until the government changes the fire safety laws,
or hotel management changes the fire-reporting protocol.
If you now have to report every fire to a new class,
say FireDepartment
,
you'll have to change all those calls to Manager
in every single one of perhaps thousands of classes.
If you miss one, you have a bug
(and perhaps a burnt-out hotel and a lawsuit).
This is how spacecraft crash,
and how large corporations misplace vast sums.
If you had thought ahead during design, though,
you could instead have had all classes pass on
any fire problem to some intermediate class,
say FireMarshal
,
which then calls Manager
.
Although this seems pointless as well as inefficient,
if you later need to call some other class to handle all fires,
you only have to change one class, FireMarshal
.
As a general rule,
the fewer unrelated things any class has to care about,
the more limited any change will need to be,
and thus the more resilient your design will be in the face of future changes.
Suppose, for example, you later discover that you have to
sometimes call the fire department (a kitchen fire),
sometimes call the manager (a fire in a dumpster),
and sometimes call the electrician (an electrical fire),
you can put all the logic about what to do with various types of fires
in the FireMarshal
class;
no other class has to care how the fires they report are actually handled.
This is a simple design pattern,
a general way to isolate stress points in your design
so that future change is easier---and
given the complexity and economic importance of problems
that information architects are asked to solve every day,
flexibility in the face of change is crucial.
Finally, no matter how sophisticated and flexible your original design is,
if it's large enough and your implemented system exists for long enough,
it will, inevitably, develop scars and warts and smelly bits
as people unfamiliar with your original elegant design
change the system to accomodate new behavior.
The old engineering saying, "If it ain't broke, don't fix it",
is true of hardware, but is less true of software
because we often ask software
to do many new things long after its initial creation.
So even if your system isn't broken,
if you expect it to be around for a long time
then if it's baroque, you should fix it.
Take the guest check-in example discussed before.
If the spectrum of hotel guests changes radically
so that over time many guests need ever more involved procedures
to verify their credit worthiness,
you may decide to modify the check-in procedure
to make future change easier.
Perhaps you decide to invent a new role, CreditCheckAgent
,
who the Deskclerk
, Manager
, and
Accountant
will delegate to on guest check-in.
Rather than spreading credit-checking knowledge around,
it alone would know about how to check
the hotel's BankAccount
and PastGuestList
, and so on.
Changing your system, however, needs to be done carefully
since several methods in otherwise unrelated classes
are intricately connected to the check-in procedure---which is what led to
the problem in the first place.
If you try to accomplish the entire change all at once
you are likely to forget something somewhere.
It's better to modify your system a tiny piece at a time
while maintaining all the rest of the original structure
until you arrive at a new structure.
This evolutionary process is called refactoring
since the task is to pick out some inflexible subsystem
and slowly move around little bits of responsiblity to other,
possibly even new, roles,
until the inflexibility goes away.
To refactor your check-in subsystem, you might, for example,
first create an empty CreditCheckAgent
role,
test that everything is still okay,
delegate credit card acceptance from
Deskclerk
to it,
test that everything is still okay,
delegate past guest list checking from Manager
to it,
test,
delegate hotel bank account checking from Accountant
to it,
test,
and so on.
Each change is tiny,
so finding errors as you go is easier
than if you'd made one huge change all at once,
so it's easy to check that the entire system still functions well
after each change
until eventually you end up with a new system.
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 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.
*/
}
}