3. Event Notification

Overview

Broadcasters and receivers in many forms are common in event-driven programs. A broadcaster is an object that randomly broadcasts messages to unknown numbers and types of receivers. Receivers are objects that are either always on-perpetually listening or polling for broadcasts-or usually off-expecting to be awakened when a broadcast occurs. Pirate radio stations, local area networks, and underground magazine publishers are examples of broadcasters, while radios, networked computers, and magazine subscribers are examples of receivers. The mouse and keyboard of a computer can also be regarded as broadcasters, while mouse and keyboard handlers can be regarded as receivers. Because broadcast messages are un-addressed and occur randomly, they are often called events. In this case receivers are called event handlers, broadcasters are called event sources, and broadcasting is called event firing.

To avoid polling, which consumes too much time and bandwidth, many platforms provide several types of event notification mechanisms. These mechanisms can be viewed as instances of various event notification design patterns. The Publisher-Subscriber pattern is a simple and prevalent example of such a pattern. As such, it provides an excellent introduction to design patterns.

After a few background remarks on design patterns, a model of a fictional power plant is described. Various power plant sensors need to be notified when the reactor gets too hot. The Publisher-Subscriber pattern is presented as a solution to the problem. Of course event notification is important at higher levels of abstraction, too. To demonstrate this point a programmable constraint network is designed and built. (Constraint networks are closely related to spread sheets, which are explored in the problem section.) The role of event notification in general and of the Publisher-Subscriber pattern in particular will be revisited by the pipeline toolkit in Chapter ? and by the application frameworks in Chapter ?.

Design Patterns

The design principles discussed in the previous chapter are merely ways to measure the quality of a given design. They don't really tell us how to create good designs. In fact, creating a design from scratch is difficult. How should we begin? Where should we begin? How can we guard against needing to redesign? How will we answer critics who ask why we chose one design and not another? What we really need are reusable designs.

Surprisingly, reusable designs exist. The idea comes from architecture, where a group of architects and city planners calling themselves New Urbanists are asking why armies of professionally trained architects and engineers equipped with modern methods and technology produce strip malls, tract homes, parking lots, and golden arches? What happened to the charming main streets, parks, cafes, churches, hotels, gardens, and homes built by our untrained, ill-equipped predecessors?

To be sure, much of the answer involves economics and politics, but in his book The Timeless Way of Building [ALX] Christopher Alexander argues that we have lost our sense of design. Having delegated the job to experts, methodologies, and regulations, we have forgotten how to build.

How do we regain this sense? Unfortunately, good design can't be explicitly defined. It is too deeply rooted in human experience to be isolated in a neat phrase. It is part of our background "knowledge," and as such, it can't be completely pulled into the foreground. (See [WIN] for more detail on this idea.) Words like living, organic, authentic, and self sustaining are close synonyms, but they fall short. In the end, Alexander calls it the quality without a name. Calling designs "patterns", he writes:

A "living" pattern reconciles all of its internal forces; it is self-sustaining, while a "dead" pattern is not. A living pattern inspires life in neighboring patterns, while a dead pattern infects and kills its neighbors.

This resonates somewhat with programming, too. A good design is almost instantly recognized by an experienced programmer. It sparks a feeling of delight and satisfaction that transcends our goals and principles. This is not to say that good design is subjective or unrecognizable. Recognizing good design is like recognizing good wine. While not everyone can taste the difference between sour grape juice and a complex cabernet at first, we can train ourselves to become connoisseurs. In the case of wine, this is a matter of tasting lots of examples of good wine, learning to distinguish subtle features, and learning to communicate with other wine experts. Alexander proposes a similar idea for architects: pattern catalogs.

A design pattern is a recurring design problem together with a generic, reusable solution. A pattern catalog collects, classifies, and names design patterns mined from well designed structures. The patterns in a catalog usually follow Alexander's problem-solution format:

Traditional Neighborhood Development [KUN]

Other Names

TND, New Urbanism, Neo-traditional Planning, Low-density Urbanism, Transit Oriented Development (TOD), civic art.

Problem

Post World War II zoning laws and property taxation policies, the rise of nation-wide chain stores, and increased reliance on automobiles has led to suburban sprawl, urban decay, and the death of traditional neighborhoods and downtown districts. These problems contribute to social and environmental problems such as crime, racism, poverty, pollution, and alienation.

Solution

1. Neighborhoods should be the basic unit of city planning. Networks of neighborhoods connected and bounded by corridors (rivers, parks, boulevards) form towns and cities.

2. A neighborhood has a well defined boundary, which is no more than a five minute walk from a focused center. Neighborhoods are mixed-use and mixed-income.

3. The primary function of the street is to define a public space. Buildings must be disciplined to honor and embellish this public space. Cars are not given precedence over people.

etc.

While the impact of pattern catalogs on architecture remains to be seen, they have had a big impact on object oriented software engineering. Inspired by Alexander's ideas, small groups of programmers have been assiduously mining and cataloging software design patterns. Several well known catalogs exist ([Go4], [POSA]), there are pattern groups (Hillside, Siemens), pattern conferences (PloP and EuroPloP), pattern web sites ([WWW 8], [WWW 9]), there is even a catalog of anti patterns [ANTI]. Pattern catalogs are now an important weapon in a programmer's arsenal of tricks and tools.

Example: Monitoring Devices

Assume we are in the early design phase of a project to provide software that will control and monitor a nuclear reactor. Here's a description of the application domain:

The most important attribute of a nuclear reactor is the temperature of its core. Reactor's provide computerized controls for decrementing and incrementing the temperature. These controls are manipulated from a remote console, which is manned by our most competent employee, Homer Simpson. Sensors are scattered throughout the power plant. These include thermometers, and various types of alarms and thermostats that go off when the reactor's temperature rises above the critical level: 1500 degrees. We need to have the ability to add more and new types of sensors and consoles to the system without modifying the reactor control code.

Here's our initial model of the domain:

Our prototype initially represents reactors, sensors, and consoles as objects within the same program that communicate by invoking methods.

class Reactor {
   private double temperature = 0;
   private final double critical = 1500;
   public boolean tooHot() {
      return critical <= temperature;
   }
   public double getTemperature() {
      return temperature;
   }
   public void inc(double amt) {
      temperature += amt;
   }
   public void dec(double amt) {
      temperature -= amt;
   }
}

It's too soon to specify the details of sensors and consoles, we can at least assume that each of these devices maintains a reference to the reactor it monitors:

// console and sensors:
class Console { Reactor myReactor; ... }
class Alarm { Reactor myReactor; ... }
class Thermometer { Reactor myReactor; .. }
class Thermostat { Reactor myReactor; ... }
// etc.

How will the reactor notify sensors of changes in its temperature? The sensors could continually poll the reactor:

double lastTemp = myReactor.getTemperature();
while(true)
{
   double temp = myReactor.getTemperature();
   if (lastTemp != temp) { /* do something */ }
   lastTemp = temp;
}

This might be possible in the final deployment where the control loop of each sensor runs on a dedicated processor, although it will generate an unacceptable level of network traffic. After all, changes in the temperature of the reactor's core will probably be relatively infrequent compared to the polling frequency. In our prototype the reactor, console, and sensors all run in the same thread of control, so polling isn't an option.

We could add a sequence of statements to Reactor.inc() and Reactor.dec() that call specific methods of each sensor:

void inc(double amt)
{
   temperature += amt;

   // there are different types of thermometers:
   thermometer1.setTemp(temperature);
   thermometer2.adjust(temperature);
   thermometer3.increment(amt);
   // etc.
   if (critical <= temperature) // yikes!
   {
      // there are different types of alarms:
      alarm1.ring();
      alarm2.flash();
      alarm3.buzz();
      // etc.
   }
}

The problem with this solution is that each time a new sensor is added to the system, we will have to add statements to inc() and dec(). This is an example of tight coupling. It creates dependencies between the reactor and the sensors, and it is exactly what the last sentence in the domain description asks us not to do. Why? The program that actually controls the reactor is probably complicated, very specialized, and, more importantly, it must never fail! Each time we allow people to modify this program when a new type of sensor is added to the system, we open the door for introducing bugs (and melt downs).

The Publisher-Subscriber Pattern

It's time to reach for a pattern catalog. The Publisher-Subscriber pattern is listed in several:

Publisher-Subscriber [Go4], [POSA], [LAR], [ROG]

Other Names

Publishers are also called senders, observables, subjects, broadcasters, and notifiers. Subscribers are also called receivers, listeners, observers and callbacks.

Problem

Various monitors need to be notified when a device changes state, but the number and types of monitors can vary dynamically. We want to avoid polling, and we don't want to make assumptions in the device code about the numbers and types of monitors.

Solution

The device should maintain a list of references to registered monitors. Monitors can be different, but each must implement the same interface, which includes an update() function that the device will call when it changes state. The list of monitor references can be managed by a reusable Publisher base class. An abstract Subscriber base class defines the interface monitors must implement.

An entry in a pattern catalog often includes a discussion of advantages and disadvantages of the pattern as well as actual examples of usage.

Static Structure

Pattern catalog entries often include a class diagram:

Subscriber is an abstract class because update() is a pure virtual function that will be implemented differently in different derived classes. Alternatively, we could have stereotyped Subscriber as an interface.

Dynamic Structure

Entries may also include sequence diagrams. Assume two monitors subscribe to a device. When the device changes state, monitor1.f() and monitor2.g() must be called:

The Publisher-Subscriber Pattern in Java

Pattern catalog entries don't usually include implementations, because they are meant to be language independent. It is the programmer's job to provide the implementation. Some patterns are so useful, they are provided in well known libraries. For example, specializations of publisher and subscriber classes are also provided in the Microsoft Foundation Class library, where they are called CDocument and CView and IBM's Visual Age library, where they are called Notifier and Subscriber. Publisher and subscriber classes are provided in the Java utility package, where they are called Observable and Observer.

Observers are subscribers:

interface Observer { // from java.util
   public void update(Observable o, Object arg);
}

Observables are publishers:

public class Observable { // from java.util
   private boolean changed = false; // = state changed?
   public void addObserver(Observer o) { ... }
   public void deleteObserver(Observer o) { ... }
   public void notifyObservers() { ... } // pull variant
   public void notifyObservers(Object arg) { ... } // push variant
   protected void setChanged() { changed = true; }
   protected void clearChanged() { changed = false; }
   public boolean hasChanged() { return changed; }
   // etc.
}

Example: Monitoring Devices (continued)

Returning to our power plant example, here is a screen shot of the reactor's console window:

The window contains an "inc" button that increments the temperature of the reactor by 500 degrees and a "dec" button that decrements the reactor's temperature by 50 degrees. In the middle, a thermometer displays the reactor's temperature. When the reactor's temperature exceeds 1500 degrees, several beeps can be heard and the message:

Warning: reactor too hot!

appears several times in the DOS/Unix console window. These actions are the result of beeping and printing alarms that monitor the reactor's temperature.

Reactor

The reactor class is almost the same as before, except it now extends Java's Observable class. The "setter" methods (i.e., those methods that modify the reactor's state) end with the sequence:

setChanged();
notifyObservers();
clearChanged();

Of course notifyObservers() calls all registered observers.

class Reactor extends Observable {
   private double temperature = 0;
   private final double critical = 1500;
   public boolean tooHot() {
      return critical <= temperature;
   }
   public double getTemperature() {
      return temperature;
   }
   public void inc(double amt) {
      temperature += amt;
      setChanged();
      notifyObservers();
      clearChanged();

   }
   public void dec(double amt) {
      temperature -= amt;
      setChanged();
      notifyObservers();
      clearChanged();

   }
}

Alarms

There are two types of subscribers: dedicated and non-dedicated. An non-dedicated subscriber can be updated by multiple publishers, while a dedicated subscriber assumes it is always updated by a particular publisher.

In our power plant prototype sensors are subscribers. We introduce three types of sensors: thermometers, printing alarms, and beeping alarms. For demonstration purposes, alarms are non-dedicated, while thermometers are dedicated.

The update() function of an non-dedicated subscriber must explicitly downcast its publisher reference parameter before it can use it. For safety, we will perform a safe cast (i.e., ask if the publisher is an instance of the Reactor class).

class BeepingAlarm implements Observer {
   public void update(Observable o, Object arg) {
      if (o instanceof Reactor) {
         Reactor r = (Reactor) o;
         if (r.tooHot()) {
            System.out.println('\u0007'); // beep
         }
      }
   }
}

class PrintingAlarm implements Observer {
   public void update(Observable o, Object arg) {
      if (o instanceof Reactor) {
         Reactor r = (Reactor) o;
         if (r.tooHot()) {
            System.out.println("Warning: reactor too hot!");
         }
      }
   }
}

Thermometers

A thermometer is a label and an observer. An instance variable references the reactor the thermometer observes. One advantage of this approach is that the thermometer constructor can perform the subscription operation on behalf of the client:

class Thermometer extends JLabel implements Observer {
   
   private Reactor myReactor;
   
   public Thermometer(Reactor r) {
      super("" + r.getTemperature());
      myReactor = r;
      myReactor.addObserver(this);
   }
      
   public void update(Observable o, Object arg) {
      setText("" + myReactor.getTemperature());   
   }
}

Reactor Console

The reactor console is a closeable frame (MainJFrame, see Programming note 3.1).

class ReactorConsole extends MainJFrame {
   private Reactor myReactor;
   // button listeners:
   class IncAction implements ActionListener { ... }
   class DecAction implements ActionListener { ... }
   // constructor:
   public ReactorConsole() { ... }
   // fire it up:
   public static void main(String[] args) {
      ReactorConsole console = new ReactorConsole();
      console.setVisible(true);
   }
}

The reactor constructor creates two printing alarms and two beeping alarms. All four alarms are added to the reactor's observer list:

   public ReactorConsole() {
      setTitle("Reactor Console");
      myReactor = new Reactor();
      BeepingAlarm ba1 = new BeepingAlarm();
      BeepingAlarm ba2 = new BeepingAlarm();
      PrintingAlarm pa1 = new PrintingAlarm();
      PrintingAlarm pa2 = new PrintingAlarm();
      myReactor.addObserver(ba1);
      myReactor.addObserver(ba2);
      myReactor.addObserver(pa1);
      myReactor.addObserver(pa2);

Next, panels holding the buttons and thermometer are created. Note that the thermometer doesn't have to be added to the reactor's observer list because the thermometer's constructor does this automatically:

      // make & add inc button panel:
      JPanel incPanel = new JPanel();
      JButton incButton = new JButton("inc");
      incButton.addActionListener(new IncAction());
      incPanel.add(incButton);
      // make & add dec button panel:
      JPanel decPanel = new JPanel();
      JButton decButton = new JButton("dec");
      decButton.addActionListener(new DecAction());
      decPanel.add(decButton);
      // make & add thermometer panel:
      JPanel thermPanel = new JPanel();
      Thermometer thermometer = new Thermometer(myReactor);
      thermPanel.add(thermometer);

Finally, the panels are added to the JFrame's content pane:

      // add panels:
      Container contentPane = getContentPane();
      contentPane.add(incPanel, "West");
      contentPane.add(thermPanel, "Center");
      contentPane.add(decPanel, "East");
   }

Java Foundation Classes (JFC)

JFC is a "framework" for building graphical user interfaces. These include the classes in the java.awt and javax.swing packages, Java Accessibility, Java 2D, Java 3D, etc. A JFC GUI is a window containing GUI components such as buttons, menus, text fields, etc.. Here are a few of the JFC component classes:

Most GUI components fire events when manipulated by users. Here are a few JFC event classes:

For example, the reactor console indirectly extends JFrame and contains two JButtons and a JLabel:

Event Delegation

When the user presses either the "inc" or "dec" button, an action event is fired and the corresponding reactor method is invoked. But how does the console know when an action event has been fired?

In JDK 1.0 every action event was routed to the other GUI components. Each component could either handle the event or ignore it. This scheme was inefficient because a lot of time was wasted routing the event to disinterested components.

In later versions of Java a strategy called event delegation was adopted. Under this scheme programmers were required to register listener objects with each component that fired events the programmer wanted to handle. When a component fires an event, the event is only routed to registered listeners. If there are no registered listeners-- if the programmer isn't interested in a particular event-- then no routing occurs. all listeners must realize one or more listener interfaces:

For example, in the reactor console constructor, an IncAction listener is added to the list of listeners for the "inc" button and a DecAction listener is added to the list of listeners for the "dec" button (these are the only listeners):

incButton.addActionListener(new IncAction());
decButton.addActionListener(new DecAction());

IncAction and DecAction are declared as inner classes of the ReactorConsole class. This is commonly done because these classes are seldom reusable outside of a particular GUI, but also because inner class instances have access to the private variables of the outer class instance that creates them. Thus, our listeners can access the myReactor reference of the reactor console that creates them:

   class IncAction implements ActionListener {
      public void actionPerformed(ActionEvent a) {
         myReactor.inc(500);
      }
   }

   class DecAction implements ActionListener {
      public void actionPerformed(ActionEvent a) {
         myReactor.dec(50);
      }
   }

Notice that this is yet another example of the Publisher-Subscriber pattern. Components are publishers. They publish events. Listeners are subscribers. They must implement methods prescribed by appropriate listener interfaces and they must register with the components they listen to. When an event is fired, the appropriate methods of every registered listener is called.

Constraint Networks

A constraint network represents a mathematical relationship between several variables, and is able to compute the value of any one of these variables given the values of all the others.

There are two types of nodes in a constraint network: cells and constraints. Cells represent variables (read-only cells represent constants) and constraints represent primitive mathematical relationships such as z = x + y and z = x * y. The neighbors of a constraint are the cells that it constrains. The neighbors of a cell are the constraints that constrain it.

A number can be saved to or erased from a cell. In either case, the cell's neighboring constraints will be notified. Upon notification that a neighboring cell has been erased, a constraint erases all of its neighbors. Upon notification that a number has been stored in a neighboring cell, a constraint attempts to compute new values for its remaining neighbors. In either case, cells updated by a constraint notify the other constraints they are connected to. In this way cell updates cascade through the network.

For example, assume x, y, and z are cells connected to an addition constraint. If a number is stored in x, and if y already holds a number, then the adder constraint stores x + y in z. However, if y doesn't hold a value, but z does, then the adder constraint stores z - x in y. Contrast this "bi-directional" behavior with the "uni-directional" behavior of the ordinary assignment statement:

z = x + y;

which only assigns x + y to z.

As a more complex example, consider the relationship:

z = 3x + 4y

We can represent this as a constraint network consisting of three constraints and seven cells:

In this network the cells labeled 3 and 4 are constant or read-only cells. The cells labeled 3x and 4y hold 3 * x and 4 * y. The nodes labeled + and * are constraints.

Assume x = 2 and y is undefined. In this case 3x = 6 and 4y is undefined. Assume 26 is stored in z. The adder constraint stores 26 - 6 = 20 in 4y. At this point the right multiplier constraint stores 20/4 = 5 in y.

ConNet

Our constraint network is called ConNet. The implementation is based on [AbS]. ConNet's console executes the following commands:

-> help
help (displays this message)
quit (terminates session)
set NAME VALUE (update a variable cell)
addVar NAME (add a variable cell)
addConst NAME VALUE (add a constant cell)
display NAME (display a cell)
undisplay NAME (undisplay a cell)
addAdder ARG1 ARG2 RESULT (add an adder constraint)
addMult ARG1 ARG2 RESULT (add a multiplier constraint)
clear (get rid of all cells & constraints)
forgetAll (forget all cell values)
forget NAME (forget a cell value)

Let's program ConNet to solve the equation:

z = 3x + 4y

We begin by creating three cells representing the variables x, y, and z:

-> addVar x
done
-> addVar y
done
-> addVar z
done

Next, we create two cells that hold the constants 3 and 4, as well as two cells to hold the intermediate values of 3x and 4y:

-> addConst 3 3
done
-> addConst 4 4
done
-> addVar 3x
done
-> addVar 4y
done

We create three constraints. The first asserts that 4y holds the product of 4 and y, the second asserts that 3x holds the product of 3 and x, and the third asserts that z holds the sum of 3x and 4y:

-> addMult y 4 4y
done
-> addMult x 3 3x
done
-> addAdder 3x 4y z
done

Normally, cells are updated quietly. If we want a cell to display itself when it is updated, we must explicitly ask it to do so. In our example, we are only interested in the values held by x, y, and z:

-> display x
done
-> display y
done
-> display z
done

Suppose we want to compute y assuming x = 3.14 and z = 9. In this case

y = (z - 3x)/4 = (9 - 3 * 3.14)/4 = -0.105

To do this, we simply set x to 3.14 and z to 9:

-> set x 3.14
x = 3.14
done
-> set z 9
z = 9
y = -0.105
done

Next, let's compute z assuming x = 5 and y = 3. In this case:

z = 3x + 4y = 3 * 5 + 4 * 3 = 27

We begin by erasing the current values held by x, y, z, 3x, and 4y:

-> forgetAll
forgetting x
forgetting y
forgetting z
done

Of course only x, y, and z inform us that they have forgotten their values. This is because we have display turned off for 3x and 4y. Finally, we set x to 5 and y to 3:

-> set x 5
x = 5
done
-> set y 3
y = 3
z = 27
done

Design

A constraint network is an instance of the ConNet class. A constraint network maintains a collection of cells and a collection of constraints. There can be many types of constraints: adders, multipliers, subtractors, dividers, etc. Each constraint maintains a collection of links to its neighboring cells and controls the values those cells may hold. However, a cell has no direct knowledge of its neighboring constraints. Instead, the Publisher-Subscriber pattern is used. Cells are publishers and neighboring constraints are subscribers. The user interface is provided through a command line console linked to the constraint network:

For example, a constraint network representing the equation:

z = x + y

would look like this in memory:

The following sequence diagram shows the constraint network setting the value of x then the value of y:

After the value of x is set, the adder is updated, but the adder does nothing because neither y nor z holds a value. After y is set, the adder is again updated. Now two of its three neighbors holds values, so the adder sets the value of z to x + y.

Programming Notes

Programming Note 3.1

MainJFrame serves as a base class for all application windows. MainJFrame extends JFrame and handles window closing events by terminating the application. MainJFrame windows are half the size of the screen and are centered on the screen. The MainJFrame class is in my util package.


package.util;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class MainJFrame extends JFrame {

   protected int screenHeight;
   protected int screenWidth;
   protected String title = "Main Window";

   private class Terminator extends WindowAdapter {
      public void WindowClosing(WindowEvent e) {
         System.exit(0);
      }
   }

   public MainJFrame() {
      addWindowListener(new Terminator());
      setTitle(title);
      Toolkit tk = Toolkit.getDefaultToolkit();
      Dimension d = tk.getScreenSize();
      screenHeight = d.height;
      screenWidth = d.width;
      setSize(screenWidth/2, screenHeight/2);
      setLocation(screenWidth/4, screenHeight/4);
   }
}

 

Example: An Adventure Game Framework

Interfaces

interface Character
{
   public int getHealth();
   public void setHealth(int h);
   public void flee();
   public String getName();
}

 

interface Lover
{
   public void kiss(Character c);
}

 

interface Hero extends Character, Lover
{
   public void fight(Attacker atk);
}

 

interface Biter
{
   public void bite(Hero h);
}

 

interface Stomper
{
   public void stomp(Hero h);
}

 

interface Attacker extends Character, Biter, Stomper
{
   public void attack(Hero h);
}

Places

class Place
{
   public Place()
   {
      maxAttackers = 10;
      numAttackers = 0;
      attacker = new Attacker[maxAttackers];
   }

   public void enter(Hero hero) // a template method
   {
      for (int i = 0; i < numAttackers; i++)
      {
         attacker[i].attack(hero);
         hero.fight(attacker[i]);
         if (hero.getHealth() < attacker[i].getHealth())
            attacker[i].flee();
         else
         {
            hero.flee();
            break;
         }
      }
    }

   public void addAttacker(Attacker atk)
   {
      if (numAttackers < maxAttackers)
         attacker[numAttackers++] = atk;
      else
         System.err.println("Room is full!");
   }

   private int numAttackers, maxAttackers;
   private Attacker[] attacker;
}

A Medieval Implementation

Monsters

class Monster implements Attacker
{

   private int health;
   private Random generator;
   private String name;

   public Monster(String nm)
   {
      name = nm;
      health = 50;
      generator = new Random();
   }

   public int getHealth() { return health; }
   public void setHealth(int h) { health = h; }
   public String getName() { return name; }
   public void flee()
   {
      System.out.println(name + " is limping from the room!");
   }

   public void stomp(Hero h)
   {
      System.out.println(name + " is stomping " + h.getName());
   }

   public void bite(Hero h)
   {
      System.out.println(name + " is biting " + h.getName());
   }

   public void attack(Hero h)
   {
      stomp(h);
      bite(h);
      stomp(h);
      bite(h);
      h.setHealth(generator.nextInt() % h.getHealth());
   }
}

Knights

class Knight implements Hero
{

   private int health;
   private Random generator;
   private String name;

   public Knight(String nm)
   {
      name = nm;
      health = 50;
      generator = new Random();
   }

   public int getHealth() { return health; }
   public void setHealth(int h) { health = h; }
   public String getName() { return name; }
   public void flee()
   {
      System.out.println(name + " is limping from the room!");
   }
   public void kiss(Character c)
   {
      System.out.println(name + " is kissing " + c.getName());
   }
   public void fight(Attacker atk)
   {
      System.out.println(name + " is fighting " + atk.getName());
      atk.setHealth(generator.nextInt() % atk.getHealth());
   }
}

The Game

class Game
{

   public static void main(String[] args)
   {
      Place dungeon = new Place();
      dungeon.addAttacker(new Monster("Godzilla"));
      dungeon.addAttacker(new Monster("Mothra"));
   
      Knight xena = new Knight("Xena");
      Knight connan = new Knight("Connan");

      dungeon.enter(xena);
      dungeon.enter(connan);

      if (connan.getHealth() > 0 && xena.getHealth() > 0)
         connan.kiss(xena);
   }
}

Program Output

C:\Pearce\Java\Projects>javac Game.java

C:\Pearce\Java\Projects>java Game
Godzilla is stomping Xena
Godzilla is biting Xena
Godzilla is stomping Xena
Godzilla is biting Xena
Xena is fighting Godzilla
Godzilla is limping from the room!
Mothra is stomping Xena
Mothra is biting Xena
Mothra is stomping Xena
Mothra is biting Xena
Xena is fighting Mothra
Mothra is limping from the room!
Godzilla is stomping Connan
Godzilla is biting Connan
Godzilla is stomping Connan
Godzilla is biting Connan
Connan is fighting Godzilla
Connan is limping from the room!

 

C:\Pearce\Java\Projects>java Game
Godzilla is stomping Xena
Godzilla is biting Xena
Godzilla is stomping Xena
Godzilla is biting Xena
Xena is fighting Godzilla
Xena is limping from the room!
Godzilla is stomping Connan
Godzilla is biting Connan
Godzilla is stomping Connan
Godzilla is biting Connan
Connan is fighting Godzilla
Godzilla is limping from the room!
Mothra is stomping Connan
Mothra is biting Connan
Mothra is stomping Connan
Mothra is biting Connan
Connan is fighting Mothra
Mothra is limping from the room!
Connan is kissing Xena

C:\Pearce\Java\Projects>

A UML Class Diagram

A Space Age Implementation

Killer Robots

class Robot implements Attacker
{

   private int health;
   private Random generator;
   private String name;

   public Robot(String nm)
   {
      name = nm;
      health = 100;
      generator = new Random();
   }

   public int getHealth() { return health; }
   public void setHealth(int h) { health = h; }
   public String getName() { return name; }
   public void flee()
   {
      System.out.println(name + " has blown a fuse!");
   }

   public void stomp(Hero h)
   {
      System.out.println(name + " is crushing " + h.getName());
   }

   public void bite(Hero h)
   {
      System.out.println(name + " is lasering " + h.getName());
   }

   public void attack(Hero h)
   {
      stomp(h);
      bite(h);
      bite(h);
      bite(h);
      h.setHealth(generator.nextInt() % h.getHealth());
   }
}

Space Men

class SpaceMan implements Hero
{

   private int health;
   private Random generator;
   private String name;

   public SpaceMan(String nm)
   {
      name = nm;
      health = 50;
      generator = new Random();
   }

   public int getHealth() { return health; }
   public void setHealth(int h) { health = h; }
   public String getName() { return name; }
   public void flee()
   {
      System.out.println(name + " is limping to the escape pod!");
   }
   public void kiss(Character c)
   {
      System.out.println(name + " is kissing " + c.getName());
   }
   public void fight(Attacker atk)
   {
      System.out.print(name + " is shooting beams at ");
      System.out.println(atk.getName());
      atk.setHealth(generator.nextInt() % atk.getHealth());
   }
}

The Game

class Game
{

   public static void main(String[] args)
   {
      Place spaceShip = new Place();
      spaceShip.addAttacker(new Robot("R2D2"));
      spaceShip.addAttacker(new Robot("Robbie"));
   
      SpaceMan buzz = new SpaceMan("Buzz");
      SpaceMan barb = new SpaceMan("Barbarella");

      spaceShip.enter(buzz);
      spaceShip.enter(barb);

      if (buzz.getHealth() > 0 && barb.getHealth() > 0)
         buzz.kiss(barb);
   }
}

Program Output

C:\Pearce\Java\Projects>javac Game.java

C:\Pearce\Java\Projects>javac Game.java

C:\Pearce\Java\Projects>java Game
R2D2 is crushing Buzz
R2D2 is lasering Buzz
R2D2 is lasering Buzz
R2D2 is lasering Buzz
Buzz is shooting beams at R2D2
Buzz is limping to the escape pod!
R2D2 is crushing Barbarella
R2D2 is lasering Barbarella
R2D2 is lasering Barbarella
R2D2 is lasering Barbarella
Barbarella is shooting beams at R2D2
Barbarella is limping to the escape pod!

 

C:\Pearce\Java\Projects>java Game
R2D2 is crushing Buzz
R2D2 is lasering Buzz
R2D2 is lasering Buzz
R2D2 is lasering Buzz
Buzz is shooting beams at R2D2
R2D2 has blown a fuse!
Robbie is crushing Buzz
Robbie is lasering Buzz
Robbie is lasering Buzz
Robbie is lasering Buzz
Buzz is shooting beams at Robbie
Buzz is limping to the escape pod!
R2D2 is crushing Barbarella
R2D2 is lasering Barbarella
R2D2 is lasering Barbarella
R2D2 is lasering Barbarella
Barbarella is shooting beams at R2D2
Barbarella is limping to the escape pod!
Buzz is kissing Barbarella

C:\Pearce\Java\Projects>

Problems

Problem 3.1

Implement and test the power plant example. Add a thermostat class to the example. When the reactor gets too hot, the thermostat is notified and repeatedly decrements the reactor temperature by 3 degrees until the temperature drops below the critical level. Be careful. Each time the thermostat calls the reactor's dec() method, the notifyObservers() method is invoked, which invokes the thermostat's update() method. Soon there will be multiple loops trying to cool the reactor, not to mention lots of extra calls to the update functions of the other subscribers.

Problem 3.2

Implement your own Publisher class and Subscriber interface. Use Java's List collection to hold subscriber references and an Iterator to notify subscribers. Test your implementation by replacing Observer by Subscriber and Observable by Publisher in the power plant example.

Problem 3.3

Finish the implementation of ConNet and test it with the equation:

z = 2x + 5y

Problem 3.4

Add a constraint to ConNet that squares a number. Test it with the equation:

a = 3.1416 * r2

Problem 3.5: Event Managers

The Publisher-Subscriber pattern is an instance of a general class of architectures called Event Notification Systems. In general, these systems provide indirect communication between sender objects (e.g., publishers) and receiver objects (e.g., subscribers). Indirect communication promotes decoupling between senders and receivers and allows senders to broadcast messages. (Events have sources but not destinations, while messages have sources and destinations.)

Event managers are another example of an event notification system. They are used when multiple devices need to be monitored. They are closely related to Application coordinators (see [LAR]), the Event Delegation Model used by Java's abstract window's toolkit (AWT), View Managers (see chapter six), and message dispatchers (see chapter seven).

Event Manager [LAR]

Other Names

Event managers are closely related to view managers, application coordinators, and message dispatchers.

Problem

Various listeners need to be notified when a certain type of event occurs. These events are "fired" by various event sources. We want to avoid polling or tight coupling between listeners and sources.

Solution

The event manager maintains a table of associations between event types and lists of registered listeners interested in occurrences of the corresponding event type. Usually there is just one event manager. When an event of type t occurs, the event manager passes the event to the handleEvent() function of each listener registered for type t events. Each event source maintains a reference to the event manager, and calls its notify() function when it fires an event.

Static Structure

Using the class diagram as a guide, implement the following classes:

class Event { ... }
class EventManager { ... }
abstract class Listener { ... }

Hint:

Try using the List interface from the Java Collections Framework to implement the event manager's registered listeners. Typical event types are strings and integers.

Test your implementation by modeling a situation where multiple sensors (listeners) monitor multiple nuclear reactors (sources). Assume nuclear reactors have two critical temperatures:

double criticalHi = 1500; // cool it down!
double criticalLo = 500; // warm it up!

Problem 3.6: Background Services

Certain background services may need to be performed each time a word processor document is modified by the user. These services include spell checking, grammar checking, repagination, backing up the document, and re-hyphenation. Of course other background services could be included as new, third-party components are added to the word processor. Naturally, the word processor document must be decoupled from the background services. Draw a UML class diagram showing how you would solve this problem. Your diagram should include the following classes: Document, SpellCheck, GrammarCheck, Paginator, BackUp, and Hyphenator.