Lesson 2 - Observer with Delegates

Now we're ready to take a step closer to using .NET events to solve our problem. In the last lesson, we used the Observer pattern to solve a problem where we needed to know when a large list of unknown classes had new data for our weather station. This was flexible and easy to implement, but we had to implement two interfaces in order to make it work. Observer had some problems, and we'd like to address them, so let's move on to discussing delegates.

What's a delegate?

Delegates can be sort of confusing if you haven't used a language that had something similar before. VB6 didn't have delegates as far as I know, but if you ever had to use an API function with callbacks then you've accidentally used something similar. C/C++ users will recognize delegates as similar to function pointers. Now I've gone and wasted the first paragraph without even defining what a delegate is!

Put simply, a delegate is a variable that can hold a method. This may not sound that useful to you, but it's probably because you've never used one. I could spend lots of time coming up with an example, but there's already a ready-made example: Array.Sort. If you look at the documentation, you'll find an overload that looks like this:

Public Sub Sort(ByVal array As System.Array, ByVal comparer As Comparison(Of T)

If you look at the documentation for Comparison(Of T), you'll see it's a delegate:

Public Delegate Function Comparison(Of T)(ByVal x As T, ByVal y As T) As Integer

The reason for all this mess is because by itself, Array.Sort isn't very powerful. The default behavior is to sort the array in ascending order using an unstable QuickSort. What if you want to sort descending? What if you're sorting objects that don't implement IComparable? By allowing you to provide a delegate to determine how two objects compare to each other, the designers of Sort gave tremendous power to the user. Think of it this way: at the heart of every array sorting algorithm is a line of code that decides how two items compare. The rest of the algorithm is just fancy tricks to try and reduce the number of these comparisons you have to make. Since the comparison is the important part of the algorithm, the designers of Sort decided to implement the comparison as a call to a delegate method that the user can change if needed. Of course, I have an example of using this overload in the ArraySortDemo project.

Your boss wants you to sort a list of employees. The Employee class does not implement IComparable, so it can't be directly supported by Sort. Let's assume that there are some very good reasons why you cannot modify Employee. It might look like you're going to be stuck writing your own sorting method, but you'd be wrong! All you really need to do is write methods that conform to the signature of Comparison(Of T). I've done just that in ArraySortDemo.

IdSorter matches Comparison(Of T). It compares Employee objects based on their Id property. This is a good starter because it's simple. NameSorter demonstrates that you aren't stuck writing simple implementations; it compares the names of two employees and sorts by last name, then first name. The application prints the list twice: once sorted by ID and once sorted by name. The key things you should note are that it was easy for me to change the behavior of Sort and that I didn't have to modify Employee so that Sort could recognize it. This is just one use of delegates.

Delegates are powerful tools. They basically let you say, "I need a method that takes these arguments and returns this value" and give you (or the user) the freedom to decide at runtime which method is appropriate. This makes delegates an appropriate tool for many scenarios where you (or your users) might need to customize behavior in an unpredictable way at runtime. It should come as no surprise that delegates are the key to several design patterns in .NET. Observer is one of them. Let's take a look at how delegates can take some of the burden of implementing Observer off our backs.

Implementing Observer with Delegates

Now we'll be looking at the project WeatherStationWithDelegates. As you might guess, we are going to try and implement our weather station using delegates. We'll see how this implementation compares to the interface-based Observer implementation, and how delegates help us avoid the need to write some of the plumbing for Observer.

The first thing you may notice is I removed the IObserver and IObservable interfaces. As I'll demonstrate, the terminology and idioms related to using delegates remove the need for these interfaces. Take a look at ISensor. First, I added a new delegate: UpdateCallback. Now ISensor's contract states that if a class wants to observe the sensor, it needs to provide a method that matches UpdateCallback's signature and register the method via AddCallback. Obviously, this is similar to IObservable, there is really no need for IObserver when you use delegates, so I removed IObservable for symmetry.

Now peek inside TemperatureSensor. There's only two real changes here. First, there's the _updateCallbacks variable that is used to maintain a list of the callback methods. Next, OnTemperatureUpdated had to be changed. Now, instead of calling the Notify method on a list of IObservers, it invokes every callback in its list. This is why IObserver is not needed when you use delegates: you can directly call the delegate, so there's no need for an interface to guarantee a method.

If you run the application, you'll see it behaves identically to the last example. You get a single window that displays a temperature, and if you click in a non-control area you get a new window that is in sync with the first. The form has to make sure to add a callback to the sensor, which it does in New.

Final Notes

I spent most of my time giving you a very brief explanation of delegates, then I briefly demonstrated how delegates remove some of the burden of implementing the Observer pattern. You may have noticed that I still have to maintain a list of callbacks and call them when the time comes. Wouldn't it be neat if something else could keep up with the list and call all of the delegates? If you think so, you'll love the next lesson!