An inside view of Observer

The Observer pattern facilitates communication between decoupled objects

Not long ago my clutch gave out, so I had my Jeep towed to a local dealership. I didn't know anybody at the dealership, and none of them knew me, so I gave them my telephone number so they could notify me with an estimate. That arrangement worked so well we did the same thing when the work was finished. Because this all turned out perfectly for me, I suspect the service department at the dealership employs the same pattern with most of its customers.

This publish-subscribe pattern, where an observer registers with a subject and subsequently receives notifications, is quite common, both in everyday life and in the virtual world of software development. In fact, the Observer pattern, as it is known, is one of the linchpins of object-oriented software development because it lets dissimilar objects communicate. That ability lets you plug objects into a framework at runtime, which allows for highly flexible, extensible, and reusable software.

Note: You can download this article's source code from Resources.

The Observer pattern

In Design Patterns, the authors describe the Observer pattern like this:

Define a one to many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

The Observer pattern has one subject and potentially many observers. Observers register with the subject, which notifies the observers when events occur. The prototypical Observer example is a graphical user interface (GUI) that simultaneously displays two views of a single model; the views register with the model, and when the model changes, it notifies the views, which update accordingly. Let's see how it works.

Observers in action

The application shown in Figure 1 contains one model and two views. The model's value, which represents image magnification, is manipulated by moving the slider knob. The views, known as components in Swing, are a label that shows the model's value and a scroll pane that scales an image in accordance with the model's value.

Figure 1. Observers in action. Click on thumbnail to view full-size image.

The model in the application is an instance of DefaultBoundedRangeModel(), which tracks a bounded integer value—in this case from 0 to 100—with these methods:

  • int getMaximum()
  • int getMinimum()
  • int getValue()
  • boolean getValueIsAdjusting()
  • int getExtent()
  • void setMaximum(int)
  • void setMinimum(int)
  • void setValue(int)
  • void setValueIsAdjusting(boolean)
  • void setExtent(int)
  • void setRangeProperties(int value, int extent, int min, int max, boolean adjusting)
  • void addChangeListener(ChangeListener)
  • void removeChangeListener(ChangeListener)

As the last two methods listed above indicate, instances of DefaultBoundedRangeModel() support change listeners. Example 1 shows how the application takes advantage of that feature:

Example 1. Two observers react to model changes

import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
public class Test extends JFrame {
      private DefaultBoundedRangeModel model =
                  new DefaultBoundedRangeModel(100,0,0,100);
      private JSlider slider = new JSlider(model);
      private JLabel readOut = new JLabel("100%");
      private ImageIcon image = new ImageIcon("shortcake.jpg");
      private ImageView imageView = new ImageView(image, model);
      public Test() {
            super("The Observer Design Pattern");
            Container contentPane = getContentPane();
            JPanel panel = new JPanel();
            panel.add(new JLabel("Set Image Size:"));
            panel.add(slider);
            panel.add(readOut);
            contentPane.add(panel, BorderLayout.NORTH);
            contentPane.add(imageView, BorderLayout.CENTER);
            model.addChangeListener(new ReadOutSynchronizer());
      }
      public static void main(String args[]) {
            Test test = new Test();
            test.setBounds(100,100,400,350);
            test.show();
      }
      class ReadOutSynchronizer implements ChangeListener {
            public void stateChanged(ChangeEvent e) {
                  String s = Integer.toString(model.getValue());
                  readOut.setText(s + "%");
                  readOut.revalidate();
            }
      }
}
class ImageView extends JScrollPane {
      private JPanel panel = new JPanel();
      private Dimension originalSize = new Dimension();
      private Image originalImage;
      private ImageIcon icon;
      public ImageView(ImageIcon icon, BoundedRangeModel model) {
            panel.setLayout(new BorderLayout());
            panel.add(new JLabel(icon));
            this.icon = icon;
            this.originalImage = icon.getImage();
            setViewportView(panel);
            model.addChangeListener(new ModelListener());
            originalSize.width = icon.getIconWidth();
            originalSize.height = icon.getIconHeight();
      }
      class ModelListener implements ChangeListener {
            public void stateChanged(ChangeEvent e) {
              BoundedRangeModel model =
               (BoundedRangeModel)e.getSource();
                  if(model.getValueIsAdjusting()) {
                        int min = model.getMinimum(), 
                              max = model.getMaximum(), 
                              span = max - min,
                              value = model.getValue();
                        double multiplier = (double)value / (double)span;
                        multiplier = multiplier == 0.0 ? 
                                           0.01 : multiplier;
                        
                        Image scaled = originalImage.getScaledInstance(
                              (int)(originalSize.width * multiplier),
                              (int)(originalSize.height * multiplier),
                              Image.SCALE_FAST);
                        icon.setImage(scaled);
                        panel.revalidate();
                        panel.repaint();
                  }
            }
      }
}

When you move the slider knob, the slider changes its model's value. That change triggers event notifications to the two change listeners registered with the model, which adjust the readout and scale the image. Both listeners use the change event passed to

stateChanged()

to determine the model's new value.

Swing is a heavy user of the Observer pattern—it implements more than 50 event listeners for implementing application-specific behavior, from reacting to a pressed button to vetoing a window close event for an internal frame. But Swing is not the only framework that puts the Observer pattern to good use—it's widely used in the Java 2 SDK; for example: the Abstract Window Toolkit, the JavaBeans framework, the javax.naming package, and input/output handlers.

Example 1 specifically shows use of the Observer pattern with Swing. Before we discuss more Observer pattern details, let's look at how the pattern is generally implemented.

How the Observer pattern works

Figure 2 shows how objects in the Observer pattern are related.

Figure 2. Observer pattern class diagram

The subject, which is an event source, maintains a collection of observers and provides methods to add and remove observers from that collection. The subject also implements a notify() method that notifies each registered observer about events that interest the observer. Subjects notify observers by invoking the observer's update() method.

Figure 3 shows a sequence diagram for the Observer pattern.

Figure 3. Observer pattern interaction. Click on thumbnail to view full-size image.

Typically, some unrelated object will invoke a subject's method that modifies the subject's state. When that happens, the subject invokes its own notify() method, which iterates over the collection of observers, calling each observer's update() method.

The Observer pattern is one of the most fundamental design patterns because it allows highly decoupled objects to communicate. In Example 1, the only thing the bounded range model knows about its listeners is that they implement a stateChanged() method. The listeners are only interested in the model's value, not how the model is implemented. The model and its listeners know very little about one another, but thanks to the Observer pattern, they can communicate. That high degree of decoupling between models and listeners lets you build software composed of pluggable objects, making your code highly flexible and reusable.

The Java 2 SDK and the Observer pattern

The Java 2 SDK provides a classic implementation of the Observer pattern with the Observer interface and the Observable class from the java.util directory. The Observable class represents the subject; observers implement the Observer interface. Interestingly, this classic Observer pattern implementation is rarely used in practice because it requires subjects to extend the Observable class. Requiring inheritance in this case is a poor design because potentially any type of object is a subject candidate, and because Java doesn't support multiple inheritance; often, those subject candidates already have a superclass.

The event-based implementation of the Observer pattern, which was used in the preceding example, is the overwhelming choice for Observer pattern implementation because it doesn't require subjects to extend a particular class. Instead, subjects follow a convention that requires the following public listener registration methods:

  • void addXXXListener(XXXListener)
  • void removeXXXListener(XXXListener)

Whenever a subject's bound property (a property that's been observed by listeners) changes, the subject iterates over its listeners and invokes the method defined by the XXXListener interface.

By now you should have a good grasp of the Observer pattern. The rest of this article focuses on some of the Observer pattern's finer points.

Anonymous inner classes

In Example 1, I used inner classes to implement the application's listeners, because the listener classes were tightly coupled to their enclosing class; however, you can implement listeners any way you desire. One of the most popular choices for handling user interface events is the anonymous inner class, which is a class with no name that's created in-line, as demonstrated in Example 2:

Example 2. Implement observers with anonymous inner classes

...
public class Test extends JFrame {
   ...
      public Test() {
         ...
            model.addChangeListener(new ChangeListener() {
                  public void stateChanged(ChangeEvent e) {
                        String s = Integer.toString(model.getValue());
                        readOut.setText(s + "%");
                        readOut.revalidate();
                  }
            });
      }
      ...
}
class ImageView extends JScrollPane {
      ...
      public ImageView(final ImageIcon icon, BoundedRangeModel model) {
      ...
            model.addChangeListener(new ChangeListener() {
                  public void stateChanged(ChangeEvent e) {
                    BoundedRangeModel model =
                     (BoundedRangeModel)e.getSource();
                        if(model.getValueIsAdjusting()) {
                              int min = model.getMinimum(), 
                                    max = model.getMaximum(), 
                                    span = max - min,
                                    value = model.getValue();
                              double multiplier = (double)value / (double)span;
                              multiplier = multiplier == 0.0 ? 
                                                             0.01 : multiplier;
                        
                              Image scaled = originalImage.getScaledInstance(
                                    (int)(originalSize.width * multiplier),
                                    (int)(originalSize.height * multiplier),
                                    Image.SCALE_FAST);
                              icon.setImage(scaled);
                              panel.revalidate();
                        }
                  }
            });
      }
}

Example 2's code is functionally equivalent to Example 1's code; however, the code above uses anonymous inner classes to define the class and create an instance in one fell swoop.

JavaBeans event handler

Using anonymous inner classes as shown in the previous example was very popular with developers, so starting with Java 2 Platform, Standard Edition (J2SE) 1.4, the JavaBeans specification has taken the responsibility for implementing and instantiating those inner classes for you with the EventHandler class, as shown in Example 3:

Example 3. Using java.beans.EventHandler

import java.beans.EventHandler;
...
public class Test extends JFrame {
   ...
      public Test() {
      ...
            model.addChangeListener(EventHandler.create(
                                    ChangeListener.class, this,
                                   "updateReadout"));
      }
   ...
      public void updateReadout() {
            String s = Integer.toString(model.getValue());
            readOut.setText(s + "%");
            readOut.revalidate();
      }
}
...

1 2 Page
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more