How far could we get trying to implement our own delegate types? How are are delegates related the observer pattern?
In this less serious but technically interesting post, we explore the relation between delegates and observers, look at how close we could and our ability to implement our own delegate type; or at least imitate our own delegate-like type. The full source code is available here: https://github.com/boegholm/DelegateDemystification
The basis for our observer pattern is the two interfaces, ISubject and IObserver
ISubjects have a number of IObserver; observers are notified using the Notify-method. IObservers can be updated using the Update-Method. This is a more or less standard definition, described in detail in various places.
If we have a delegate, say an
Action a =. Then we can assign a function to a, for example an anonymous function, such as a = ()=> Console.WriteLine(“Hello Observer”);
We can now call the function using a.Invoke() or just a(). This is almost exactly what we can do with implementations of the above interfaces. In such case, Action would be ISubject, with Notify being the equivalent of Invoke. The method on the left hand side of the assignment is the IObserver, and Update would contain the Console.WriteLine call.
With this in mind, the question now is, how close to a = <lambda> can we get with an observer pattern implementation? This is what we want to be able to write, with semantics as close to the C# delegate implementation:
In a classic observer pattern implementation, we would have to register observers with subjects, involving class implementations and initialization. Essentially the same thing, but with bulky syntax.
Wrapping delegates: RelayObserver
We start by creating RelayObserver to wrap Action delegates.
This will allow us to translate action delegates to observers by wrapping the delegate and invoke it on updates. This means that we can write:
new RelayObserver(() => Console.WriteLine()).Update();
We also make sure, that equality comparison is based on action equality.
We crate a single concrete subject implementation, with a standard notify-implementation. Its equality is based on the sequence equality of observers, in the same way as standard delegate equality works. Remember, most observers, if not all, will be RelayObservers in our experiment, which means delegate equality. Theimplementation looks like this:
The three constructors allow us to create Subject from single a single or multiple observers, and from an action. The registered observers are read only. In the end, subjects will behave almost like value-types, as delegates do. Remember, compound assignment
+= creates a new value, it does not modify the original. Same goes for
Delegate operations: operator overloading
We now have most of the machinery set, and now all we need is the syntax. We do this, by adding operator overloading to the ISubject interface (hence the partial interface definition in the previous snippet). The code for these overloads are found in the following listing:
We two overloads for + and two for -. This is in fact all we need to get pretty close to the original delegate syntax in C#. The operator implementations differ only in the whether an observer is added or removed. We can however, add or remove both Subjects and Actions. The former just add or remove all observers from the added subject. This allows us to combine two Subjects, as we can do with delegates.
If we add to a null-Subject, we just return the newly added. This is important, since we start out with no observers, meaning that the subject itself is null. Consistent with delegates.
If we add null, we just ignore the add and return whatever we added to.
If we actually add something to something that is not null, we create a new subject, and combine the observers of the two. We do not modify, but create a new instance. Again, consistent with delegates.
Done! now we can use the observer implementation with the ordinary C# syntax!
Test cases for validation
Consider the following testcases, for what we can actually do. The main difference between delegates and our observer pattern implementation is, that we have to call Notify, since we cannot overload application (). Equality operators also pose a problem, since we cannot overload these on infterfaces… yet! – but that is something for a future post. Otherwise, we are pretty close:
These test cases show examples of most of our desired properties, and through a few examples, verify the resulting semantics of our implementation.