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:

Main

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

Solution: The Publisher Subscriber pattern

Main

A Java Implementation

In Java Publisher is Observable and Subscriber is Observer.

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");
   }