< Day Day Up > |
The responsibilities of the subject class in the Observer pattern are as follows :
To create the subject class, we'll follow Java's implementation and define a base class, Observable , that any would-be subject class can extend. The Observable class provides the basic services required of all subject classes. Specifically, Observable defines the following properties and methods:
Now let's turn to the observer classes. The sole responsibility of all observer classes is to provide a standard mechanism for receiving updates. Certainly, an observer class can (and typically does) implement other behaviors according to a particular situation. For example, our earlier TextReport class implements methods that can display text on screen. However, that extra behavior is not a requirement of the Observer pattern. In the Observer pattern, the sole requirement of an observer class is the ability to receive updates. In order to guarantee that every observer class provides a standard update mechanism, we'll create an Observer interface that defines an update( ) method. Classes that wish to register with a subject (using addObserver( ) ) must implement the Observer interface and define an update( ) method. When the subject changes state (i.e., has a message to broadcast), it calls notifyObservers( ) , which invokes update( ) on all observers. Each observer's update( ) method responds to the change as it sees fit. If you're thinking ahead, you may already be wondering how the observers know what changed in the subject. For example, in our hypothetical weather application, the subject, WeatherReporter , sends an update whenever the weather report changes. How do the observers ( TextReport and GraphicReport ) obtain that updated weather information? There are two options, known as the push and pull models. In the push model, the updated information is pushed to the observers via an info object . The info object is sent to observers by the subject's notifyObservers( ) method. Each observer receives the info object as an argument to its update( ) method. For example, if the WeatherReporter class were to use the push model, it would report weather changes by invoking update( ) on TextReport and GraphicReport , as usual, but would also pass update( ) an object with, say, temperature and precipitation properties. The TextReport and GraphicReport classes would process the object in their respective update( ) methods. In the pull model, the observers are expected to retrieve the information they want via getter methods defined by the subject. For example, if the WeatherReporter class were to use the pull model, it might define methods such as getTemperature( ) and getPrecipitation( ) . The TextReport and GraphicReport classes would then be responsible for implementing an update( ) method that used getTemperature( ) and getPrecipitation( ) to retrieve the latest weather information. Both the push and pull models have their pros and cons. The pull model requires observers to manually determine what has changed every time an update is received, which can be inefficient. In the push model, observers are told explicitly what changed, but that requires the subject to know the needs of its observers. The more the subject must know about its observers, the harder it is to modify observers without affecting the subject. Our implementation of the Observer pattern supports both push and pull models. That is, the basic facilities required to send an info object at notification time are available but optional. Figure 16-1Figure 16-1 shows the architecture of our Observer implementation. The Observable class is the base class for all subjects. Subject classes (such as WeatherReporter ) extend Observable . Observer classes (such as TextReport ) implement the Observer interface and use addObserver( ) to register with the subject. When a change in the subject occurs, the subject class invokes update( ) on all Observer instances in its observers array. Example 16-1 shows the source code for the Observable class, which we've chosen to place in the util package (following Java's lead). In addition to the features we've discussed, the Observable class in Example 16-1 provides several other conveniences :
Figure 16-2. Observer pattern implementationFor an explanation of the Observable class's source code, consult the comments in Example 16-1. Note, however, that you don't need to know Observable 's internals very deeply. The Observable class is a reusable utility that never changes. We treat it as a black box that we simply extend when implementing the Observer pattern. Example 16-1. The Observable classimport util.Observer; /** * A Java-style Observable class used to represent the "subject" * of the Observer design pattern. Observers must implement the Observer * interface and register to observe the subject via addObserver( ) . */ class util.Observable { // A flag indicating whether this object has changed. private var changed:Boolean = false; // A list of observers. private var observers:Array; /** * Constructor function. */ public function Observable ( ) { observers = new Array( ); } /** * Adds an observer to the list of observers. * @param o The observer to be added. */ public function addObserver(o:Observer):Boolean { // Can't add a null observer. if (o == null) { return false; } // Don't add an observer more than once. for (var i:Number = 0; i < observers.length; i++) { if (observers[i] == o) { // The observer is already observing, so quit. return false; } } // Put the observer into the list. observers.push(o); return true; } /** * Removes an observer from the list of observers. * * @param o The observer to remove. */ public function removeObserver(o:Observer):Boolean { // Find and remove the observer. var len:Number = observers.length; for (var i:Number = 0; i < len; i++) { if (observers[i] == o) { observers.splice(i, 1); return true; } } return false; } /** * Tell all observers that the subject has changed. * * @param infoObj An object containing arbitrary data * to pass to observers. */ public function notifyObservers(infoObj:Object):Void { // Use a null info object if none is supplied. if (infoObj == undefined) { infoObj = null; } // If the subject hasn't changed, don't bother notifying observers. if (!changed) { return; } // Make a copy of the observers array. We do this to ensure // the list doesn't change while we're processing it. var observersSnapshot:Array = observers.slice(0); // This change has been processed, so unset the changed flag. clearChanged( ); // Invoke update( ) on all observers. Count backward because // it's faster, and order doesn't matter in this case. for (var i:Number = observersSnapshot.length-1; i >= 0; i--) { observersSnapshot[i].update(this, infoObj); } } /** * Removes all observers from the observer list. */ public function clearObservers( ):Void { observers = new Array( ); } /** * Indicates that the subject has changed. */ private function setChanged( ):Void { changed = true; } /** * Indicates that the subject has either not changed or * has notified its observers of the most recent change. */ private function clearChanged( ):Void { changed = false; } /** * Checks if the subject has changed. * * @return true if the subject has changed, * as determined by setChanged( ) . */ public function hasChanged( ):Boolean { return changed; } /** * Returns the number of observers in the observer list. * * @return An integer: the number of observers for this subject. */ public function countObservers( ):Number { return observers.length; } } Example 16-2 shows the source code for the Observer interface, which resides in the util package along with Observable . The Observer interface is simple, containing only one method: update( ) . We use Observer to guarantee that any class that registers for updates from a subject implements the standard update( ) method. Notice that the update( ) method defines two parameters, o and infoObj . The parameter o contains a reference to the subject that changed. It is used to distinguish changes in one subject from changes in another, as well as to access changed data in the subject (if the subject is using the pull model to update its observers). The parameter infoObj receives the optional info object provided by the subject if the subject is using the push model to broadcast changes to its observers. Note that although a single observer can, in theory, register with multiple subjects (and our code accommodates such a circumstance), in practice, a single observer typically registers with only one subject. An observer registered with multiple subjects must use awkward conditionals ( if/else or switch statements) to distinguish between those subjects at update time. When a program reaches that level of complexity, it's often better to implement full-scale event handling, as described in Chapter 19. Example 16-2. The Observer interfaceimport util.Observable; /** * The interface that must be implemented by all observers of an * Observable object. */ interface util.Observer { /** * Invoked automatically by an observed object when it changes. * * @param o The observed object (an instance of Observable ). * @param infoObj An arbitrary data object sent by * the observed object. */ public function update(o:Observable, infoObj:Object):Void; } |
< Day Day Up > |