16.10 Implement the Observer Pattern


Problem

You need to implement an efficient mechanism for an object (the subject) to notify other objects (the observers) about changes to its state.

Solution

Implement the Observer Pattern using delegate types as type-safe function pointers and event types to manage and notify the set of observers.

Discussion

The traditional approach to implementing the Observer Pattern is to implement two interfaces: one to represent an observer ( IObserver ) and the other to represent the subject ( ISubject ). Objects that implement IObserver register with the subject, indicating that they want to be notified of important events (such as state changes) affecting the subject. The subject is responsible for managing the list of registered observers and notifying them in response to events affecting the subject. The subject usually notifies observers by calling a Notify method declared in the IObserver interface. The subject might pass data to the observer as part of the Notify method, or the observer might need to call a method declared in the ISubject interface to obtain additional details about the event.

Although you are free to implement the Observer Pattern in C# using the approach just described, the Observer Pattern is so pervasive in modern software solutions that C# and the .NET Framework include event and delegate types to simplify its implementation. The use of events and delegates means that you don't need to declare IObserver and ISubject interfaces. In addition, you don't need to implement the logic necessary to manage and notify the set of registered observersthe area where most coding errors occur.

The .NET Framework uses one particular implementation of the event-based and delegate-based Observer Pattern so frequently that it has been given its own name the Event pattern . (Pattern purists might prefer the name Event idiom , but I will stick to the name most commonly used in Microsoft documentation.)

The ObserverExample.cs file in the sample code for this chapter contains a complete implementation of the Event pattern. The example contains the following types:

  • Thermostat class (the subject of the example), which keeps track of the current temperature and notifies observers when a temperature change occurs.

  • TemperatureChangeEventArgs class, which is a custom implementation of the System.EventArgs class used to encapsulate temperature change data for distribution during the notification of observers.

  • TemperatureEventHandler delegate, which defines the signature of the method that all observers of a Thermostat object must implement, and which a Thermostat object will call in the event of temperature changes.

  • TemperatureChangeObserver and TemperatureAverageObserver classes, which are observers of the Thermostat class.

The TemperatureChangeEventArgs class (in the following listing) derives from the class System.EventArgs . The custom event argument class should contain all of the data that the subject needs to pass to its observers when it notifies them of an event. If you don't need to pass data with your event notifications, you don't need to define a new argument class; simply pass a null argument when you raise the event. See recipe 16.8 for details on implementing custom event argument classes.

 // An event argument class that contains information about a temperature  // change event. An instance of this class is passed with every event. public class TemperatureChangeEventArgs : System.EventArgs {     // Private data members contain the old and new temperature readings     private readonly int oldTemperature, newTemperature;          // Constructor that takes the old and new temperature values     public TemperatureChangeEventArgs(int oldTemp, int newTemp) {              oldTemperature = oldTemp;         newTemperature = newTemp;     }          // Read-only properties provide access to the temperature values     public int OldTemperature { get { return oldTemperature; } }     public int NewTemperature { get { return newTemperature; } } } 

The following code shows the declaration of the TemperatureEventHandler delegate. Based on this declaration, all observers must implement a method (the name is unimportant), which returns void and takes two arguments: a Thermostat object as the first argument and a TemperatureChangeEventArgs object as the second. During notification, the Thermostat argument refers to the Thermostat object that raises the event, and the TemperatureChangeEventArgs argument contains data about the old and new temperature values.

 // A delegate that specifies the signature that all temperature event  // handler methods must implement public delegate void TemperatureEventHandler(Thermostat s,      TemperatureChangeEventArgs e); 

The Thermostat class is the observed object in this Observer (Event) pattern. In theory, a monitoring device sets the current temperature by calling the Temperature property on a Thermostat object. This causes the Thermostat object to raise its TemperatureChange event and send a TemperatureChangeEventArgs object to each observer. Here is the code for the Thermostat class.

 // A class that represents a thermostat, which is the source of temperature // change events. In the "Observer" pattern, a thermostat object is the  // "Subject" that "Observers" listen to for change notifications. public class Thermostat {          // Private field to hold current temperature     private int temperature = 0;          // The event used to maintain a list of observer delegates and raise     // a temperature change event when a temperature change occurs     public event TemperatureEventHandler TemperatureChange;     // A protected method used to raise the TemperatureChange event.      // Because events can be triggered only from within the containing      // type, using a protected method to raise the event allows derived      // classes to provide customized behavior and still be able to raise     // the base class event.     virtual protected void RaiseTemperatureEvent         (TemperatureChangeEventArgs e) {                  // Notify all observers. A test for null indicates whether any         // observers are registered         if (TemperatureChange != null) {                          TemperatureChange(this, e);         }     }              // Public property to get and set the current temperature. The "set"      // side of the property is responsible for raising the temperature     // change event to notify all observers of a change in temperature.     public int Temperature {                  get { return temperature; }                  set {             // Create a new event argument object containing the old and              // new temperatures             TemperatureChangeEventArgs e =                  new TemperatureChangeEventArgs(temperature, value);                          // Update the current tempertature             temperature = value;                          // Raise the temperature change event             RaiseTemperatureEvent(e);         }     } } 

For the purpose of demonstrating the Observer Pattern, the example contains two different observer types: TemperatureAverageObserver and TemperatureChangeObserver . Both classes have the same basic implementation. TemperatureAverageObserver keeps a count of the number of temperature change events and the sum of the temperature values and displays an average temperature when each event occurs. TemperatureChangeObserver displays information about the change in temperature each time a temperature change event occurs.

The following listing shows the TemperatureChangeObserver class. (See the sample file for TemperatureAverageObserver code.) Notice that the constructor takes a reference to the Thermostat object that the TemperatureChangeObserver object should observe. When you instantiate an observer, pass it a reference to the subject. The observer must create a delegate instance containing a reference to the observer's event-handler method. To register as an observer, the observer object must then add its delegate instance to the subject using the subject's public event member.

Once the TemperatureChangeObserver object has registered its delegate instance with the Thermostat object, you need to maintain a reference to the delegate only if you want to stop observing the subject later. In addition, there's no need to maintain a reference to the subject because a reference to the event source is included as the first argument each time the Thermostat object raises an event through the TemperatureChange method.

 // A Thermostat observer that displays information about the change in // temperature when a temperature change event occurs public class TemperatureChangeObserver {          // A constructor that takes a reference to the Thermostat object that     // the TemperatureChangeObserver object should observe      public TemperatureChangeObserver(Thermostat t) {                  // Create a new TemperatureEventHandler delegate instance and          // register it with the specified Thermostat         t.TemperatureChange +=              new TemperatureEventHandler(this.TemperatureChange);     }              // The method to handle temperature change events     public void TemperatureChange(Thermostat sender,          TemperatureChangeEventArgs temp) {                      System.Console.WriteLine             ("ChangeObserver: Old={0}, New={1}, Change={2}",             temp.OldTemperature, temp.NewTemperature,              temp.NewTemperature - temp.OldTemperature);     } } 

The Thermostat class defines a Main method (shown here) that drives the example. After creating a Thermostat object and two different observer objects, the Main method repeatedly prompts you to enter a temperature. Each time you enter a new temperature, the Thermostat object notifies the listeners, which display information to the console.

 public static void Main() {     // Create a Thermostat instance     Thermostat t = new Thermostat();              // Create the Thermostat observers     new TemperatureChangeObserver(t);     new TemperatureAverageObserver(t);              // Loop, getting temperature readings from the user.     // Any non-integer value will terminate the loop.     do {                      System.Console.Write("\n\rEnter current temperature: ");                      try {             // Convert the user's input to an integer and use it to set             // the current temperature of the thermostat             t.Temperature =                  System.Int32.Parse(System.Console.ReadLine());                          } catch (System.Exception) {             // Use the exception condition to trigger termination                          System.Console.WriteLine("Terminating ObserverExample.");             return;         }     } while (true); } 

The following listing shows the kind of output you should expect if you build and run ObserverExample.cs. The bolded values show your input.

 Enter current temperature:  50  ChangeObserver: Old=0, New=50, Change=50 AverageObserver: Average=50.00 Enter current temperature:  20  ChangeObserver: Old=50, New=20, Change=-30 AverageObserver: Average=35.00 Enter current temperature:  40  ChangeObserver: Old=20, New=40, Change=20 AverageObserver: Average=36.67 



C# Programmer[ap]s Cookbook
C# Programmer[ap]s Cookbook
ISBN: 735619301
EAN: N/A
Year: 2006
Pages: 266

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net