Let's start our exploration of events at the basis for their implementation: the Observer pattern. Observer is a design pattern. To borrow the definiton from Head-First Design Patterns, a design pattern is a solution to a problem in a specific context. Design patterns almost always result in more flexible code at the expense of increased complexity. Design patterns tend to be language-agnostic, so it's rare that learning to use one doesn't benefit you in all cases. Let's talk about Observer.
Observer is a design pattern that you use when you have a class that needs to notify other classes when something happens, but you either don't know or don't want to limit the classes that can receive the notification. The image below is a pseudo-UML diagram that describes the Observer pattern.
The class that wants to notify other classes is called the target. In order for it
to post these notifications, we have to provide an interface that allows classes to indicate
they are interested in the notifications; this is the purpose of the
IObservable
interface (I'll discuss the purpose of each
method in a moment.) The classes that are interested in notifications are called
subjects. In order for the target to notify the subjects, the subjects must have
a method that the target can call. This is the purpose for the
IObserver
interface.
Before we get into the technical details, let's take a moment to discuss how these names
help describe the situation in case it got lost in the paragraph above. The target
wants to notify other types when something happens; to indicate that it wants to do this it
implements IObservable
. An observable object is one that
can be observed. The objects that want to observe the target are called subjects,
and in order to be able to observe the target they have to implement
IObserver
. So, observers exist to observe; observables exist
to be observed. Make sense?
Now for the technical details. Notice that IObserver
provides the Update()
method. This is the method that the target must call
whenever it needs to notify its subjects. When any subject wants to be notified, it calls
the target's AddListener()
method so that it will be added to the list of
IObserver
objects that will be notified. This is why we need
IObserver
: through interface implementation we can have our
target notify classes of any type that implements the interface, even if the type is one
we've never heard of. Notice the Notify()
method provided by
IObserver
; when the target needs to notify its subjects, it
will call this method. (Side note: technically, I would argue that
Notify()
should be a private or protected method because only the target
should be able to notify subjects that something has happened. Unfortunately, you cannot
define a private method with an interface. Just consider Notify()
's presence
in the diagram as a kind of helper to remind you that it is important to the pattern.)
A typical implementation of Notify()
might look like this:
Private Sub Notify() For Each subject In _subjects subject.Update() Next End Sub
Since all of the subjects implement IObserver
, they all have
an Update()
method. The subject responds to Update()
however it
wishes. The important thing to take away here is the Observer pattern enables you to
send a notification to a wide variety of classes that you don't know about at design time.
You'll find the example in the WeatherStationWithObserver project in the example solution. Since it's the first time you've seen the application, let's spend some time talking about what it does before I go over how it implements the solution using the Observer pattern.
You've been tasked with writing a weather station application (side note: I lifted the spirit of this example from the book Head-First Design Patterns; it's not identical but I felt like being honest.) The weather station consists of two parts: a base station that displays information to the user and modular sensors that plug in to the base station. Users purchase the base station and any number of sensors. Sensors can be made by third parties, but they will be guaranteed to implement any interface you deem mandatory. Sensors will update at intervals determined by the manufacturer, and your base station needs to update immediately when a sensor provides new data. It just so happens the company has a temperature sensor they want you to implement the code for so you can decide on an interface that sensors should implement.
This is perfect for the Observer pattern. Your base station should be a subject, and the sensors should be the targets. The base station will subscribe to any sensors that are connected, and when a sensor updates it will update its display. For the purposes of simplicity, I am going to implement the example as if only the temperature sensor is supported, then discuss how it could have been changed to support any type of sensor (which will give you an idea of why it would have made the example less clear.)
I'm going to assume you get IObserver
and
IObservable
and skip on to
ISensor
, since the first two are on the diagram and are
exactly as indicated. ISensor
is the interface that we will
require out of every sensor that wants to be compatible with the base station. We will
require that the sensors implement IObservable
and provide a
Name
and their Data
as a
Double. Since every sensor will implement this interface, the base station has a fighting
chance of displaying information about the sensor.
Let's look at TemperatureSensor
next. It implements
ISensor
to respect the contract the base station requires.
AddListener()
and RemoveListener()
simply add or remove the
IObserver
to the private list of observers. The temperature
sensor itself starts a timer that ticks every second. When the timer ticks, the sensor
makes a new reading and notifies any observers that a new temperature reading is available
by calling Notify()
. Since this is just an example, all it does is generate a
random temperature between 70 and 90 degrees (Fahrenheit).
Now, let's look at MainForm
, which implements the base station since I'm not
trying to overwhelm you with a full-fledged presentation model that avoids implementing
logic in its UI. There's two constructors in MainForm
, because I wanted to
demonstrate a feature. The constructor that takes a TemperatureSensor
parameter subscribes to the sensor's updates. The parameterless constructor creates a new
TemperatureSensor
and passes this to the other constructor. When the sensor
calls the form's Update()
method, the form gets the latest temperature reading
and displays it. Note that the sensor's Name
property is used to fill in the
label above the temperature reading.
Now for the final feature, and the reason for the extra constructor. When you click on the form (which is kind of hard; I put the ugly border around the temperature to give you a fighting chance) another form is displayed, and it uses the same sensor as the first form.
Something I decided to omit from the example because it would have hidden the concepts beneath some needless complexity is the ability to support multiple sensors. You can do this yourself if you feel like testing your knowledge. The first hurdle is the form itself needs a collection to store more than one sensor, and likely a pair of add/remove methods to implement connecting and disconnecting sensors. The elaborate part of the project involves displaying the data from the sensors. I'd do this by creating a user control based on the two labels and using a hashtable to link controls to their sensors. Obviously, this might have left you scratching your head over implementation details that have nothing to do with the subject of this demo.
We had to do a good bit of work to implement the Observer pattern, but the end result is
we have a base weather station that can support sensors we don't know about so long as the
sensors implement the ISensor
interface. The sensors can
update whenever they want, so long as they notify our base station that their data has
updated.
In the next demo, we'll discuss how we can make the implementation of our weather station a little more simple if we modify our interpretation of the Observer pattern to use delegates, a powerful .NET feature that's going to remove some of the burden from our backs.