C#: Handling Notifications: inheritance, events, or delegates?

Posted by James Michael Hare on Geeks with Blogs See other posts from Geeks with Blogs or by James Michael Hare
Published on Tue, 18 May 2010 16:10:10 GMT Indexed on 2010/05/19 12:40 UTC
Read the original article Hit count: 386

Filed under:

Often times as developers we have to design a class where we get notification when certain things happen. In older object-oriented code this would often be implemented by overriding methods -- with events, delegates, and interfaces, however, we have far more elegant options. So, when should you use each of these methods and what are their strengths and weaknesses?

Now, for the purposes of this article when I say notification, I'm just talking about ways for a class to let a user know that something has occurred. This can be through any programmatic means such as inheritance, events, delegates, etc.

So let's build some context. I'm sitting here thinking about a provider neutral messaging layer for the place I work, and I got to the point where I needed to design the message subscriber which will receive messages from the message bus. Basically, what we want is to be able to create a message listener and have it be called whenever a new message arrives. Now, back before the flood we would have done this via inheritance and an abstract class:

   1:  
   2: // using inheritance - omitting argument null checks and halt logic
   3: public abstract class MessageListener
   4: {
   5:     private ISubscriber _subscriber;
   6:     private bool _isHalted = false;
   7:     private Thread _messageThread;
   8:  
   9:     // assign the subscriber and start the messaging loop
  10:     public MessageListener(ISubscriber subscriber)
  11:     {
  12:         _subscriber = subscriber;
  13:         _messageThread = new Thread(MessageLoop);
  14:         _messageThread.Start();
  15:     }
  16:  
  17:     // user will override this to process their messages
  18:     protected abstract void OnMessageReceived(Message msg);
  19:  
  20:     // handle the looping in the thread
  21:     private void MessageLoop()
  22:     {
  23:         while(!_isHalted)
  24:         {
  25:             // as long as processing, wait 1 second for message
  26:             Message msg = _subscriber.Receive(TimeSpan.FromSeconds(1));
  27:             if(msg != null)
  28:             {
  29:                 OnMessageReceived(msg);
  30:             }
  31:         }
  32:     }
  33:     ...
  34: }

It seems so odd to write this kind of code now. Does it feel odd to you? Maybe it's just because I've gotten so used to delegation that I really don't like the feel of this. To me it is akin to saying that if I want to drive my car I need to derive a new instance of it just to put myself in the driver's seat. And yet, unquestionably, five years ago I would have probably written the code as you see above.

To me, inheritance is a flawed approach for notifications due to several reasons:

  • Inheritance is one of the HIGHEST forms of coupling.
  • You can't seal the listener class because it depends on sub-classing to work.
  • Because C# does not allow multiple-inheritance, I've spent my one inheritance implementing this class.
  • Every time you need to listen to a bus, you have to derive a class which leads to lots of trivial sub-classes.
  • The act of consuming a message should be a separate responsibility than the act of listening for a message (SRP).

Inheritance is such a strong statement (this IS-A that) that it should only be used in building type hierarchies and not for overriding use-specific behaviors and notifications. Chances are, if a class needs to be inherited to be used, it most likely is not designed as well as it could be in today's modern programming languages.

So lets look at the other tools available to us for getting notified instead. Here's a few other choices to consider.

  • Have the listener expose a MessageReceived event.
  • Have the listener accept a new IMessageHandler interface instance.
  • Have the listener accept an Action<Message> delegate.

Really, all of these are different forms of delegation. Now, .NET events are a bit heavier than the other types of delegates in terms of run-time execution, but they are a great way to allow others using your class to subscribe to your events:

   1: // using event - ommiting argument null checks and halt logic
   2: public sealed class MessageListener
   3: {
   4:     private ISubscriber _subscriber;
   5:     private bool _isHalted = false;
   6:     private Thread _messageThread;
   7:  
   8:     // assign the subscriber and start the messaging loop
   9:     public MessageListener(ISubscriber subscriber)
  10:     {
  11:         _subscriber = subscriber;
  12:         _messageThread = new Thread(MessageLoop);
  13:         _messageThread.Start();
  14:     }
  15:  
  16:     // user will override this to process their messages
  17:     public event Action<Message> MessageReceived;
  18:  
  19:     // handle the looping in the thread
  20:     private void MessageLoop()
  21:     {
  22:         while(!_isHalted)
  23:         {
  24:             // as long as processing, wait 1 second for message
  25:             Message msg = _subscriber.Receive(TimeSpan.FromSeconds(1));
  26:             if(msg != null && MessageReceived != null)
  27:             {
  28:                 MessageReceived(msg);
  29:             }
  30:         }
  31:     }
  32: }

Note, now we can seal the class to avoid changes and the user just needs to provide a message handling method:

   1: theListener.MessageReceived += CustomReceiveMethod;

However, personally I don't think events hold up as well in this case because events are largely optional. To me, what is the point of a listener if you create one with no event listeners? So in my mind, use events when handling the notification is optional.

So how about the delegation via interface? I personally like this method quite a bit. Basically what it does is similar to inheritance method mentioned first, but better because it makes it easy to split the part of the class that doesn't change (the base listener behavior) from the part that does change (the user-specified action after receiving a message).

So assuming we had an interface like:

   1: public interface IMessageHandler
   2: {
   3:     void OnMessageReceived(Message receivedMessage);
   4: }

Our listener would look like this:

   1: // using delegation via interface - omitting argument null checks and halt logic
   2: public sealed class MessageListener
   3: {
   4:     private ISubscriber _subscriber;
   5:     private IMessageHandler _handler;
   6:     private bool _isHalted = false;
   7:     private Thread _messageThread;
   8:  
   9:     // assign the subscriber and start the messaging loop
  10:     public MessageListener(ISubscriber subscriber, IMessageHandler handler)
  11:     {
  12:         _subscriber = subscriber;
  13:         _handler = handler;
  14:         _messageThread = new Thread(MessageLoop);
  15:         _messageThread.Start();
  16:     }
  17:  
  18:     // handle the looping in the thread
  19:     private void MessageLoop()
  20:     {
  21:         while(!_isHalted)
  22:         {
  23:             // as long as processing, wait 1 second for message
  24:             Message msg = _subscriber.Receive(TimeSpan.FromSeconds(1));
  25:             if(msg != null)
  26:             {
  27:                 _handler.OnMessageReceived(msg);
  28:             }
  29:         }
  30:     }
  31: }

And they would call it by creating a class that implements IMessageHandler and pass that instance into the constructor of the listener. I like that this alleviates the issues of inheritance and essentially forces you to provide a handler (as opposed to events) on construction.

Well, this is good, but personally I think we could go one step further. While I like this better than events or inheritance, it still forces you to implement a specific method name. What if that name collides? Furthermore if you have lots of these you end up either with large classes inheriting multiple interfaces to implement one method, or lots of small classes. Also, if you had one class that wanted to manage messages from two different subscribers differently, it wouldn't be able to because the interface can't be overloaded.

This brings me to using delegates directly. In general, every time I think about creating an interface for something, and if that interface contains only one method, I start thinking a delegate is a better approach. Now, that said delegates don't accomplish everything an interface can. Obviously having the interface allows you to refer to the classes that implement the interface which can be very handy. In this case, though, really all you want is a method to handle the messages. So let's look at a method delegate:

   1: // using delegation via delegate - omitting argument null checks and halt logic
   2: public sealed class MessageListener
   3: {
   4:     private ISubscriber _subscriber;
   5:     private Action<Message> _handler;
   6:     private bool _isHalted = false;
   7:     private Thread _messageThread;
   8:  
   9:     // assign the subscriber and start the messaging loop
  10:     public MessageListener(ISubscriber subscriber, Action<Message> handler)
  11:     {
  12:         _subscriber = subscriber;
  13:         _handler = handler;
  14:         _messageThread = new Thread(MessageLoop);
  15:         _messageThread.Start();
  16:     }
  17:  
  18:     // handle the looping in the thread
  19:     private void MessageLoop()
  20:     {
  21:         while(!_isHalted)
  22:         {
  23:             // as long as processing, wait 1 second for message
  24:             Message msg = _subscriber.Receive(TimeSpan.FromSeconds(1));
  25:             if(msg != null)
  26:             {
  27:                 _handler(msg);
  28:             }
  29:         }
  30:     }
  31: }

Here the MessageListener now takes an Action<Message>.  For those of you unfamiliar with the pre-defined delegate types in .NET, that is a method with the signature: void SomeMethodName(Message).

The great thing about delegates is it gives you a lot of power. You could create an anonymous delegate, a lambda, or specify any other method as long as it satisfies the Action<Message> signature. This way, you don't need to define an arbitrary helper class or name the method a specific thing.

Incidentally, we could combine both the interface and delegate approach to allow maximum flexibility. Doing this, the user could either pass in a delegate, or specify a delegate interface:

   1: // using delegation - give users choice of interface or delegate
   2: public sealed class MessageListener
   3: {
   4:     private ISubscriber _subscriber;
   5:     private Action<Message> _handler;
   6:     private bool _isHalted = false;
   7:     private Thread _messageThread;
   8:  
   9:     // assign the subscriber and start the messaging loop
  10:     public MessageListener(ISubscriber subscriber, Action<Message> handler)
  11:     {
  12:         _subscriber = subscriber;
  13:         _handler = handler;
  14:         _messageThread = new Thread(MessageLoop);
  15:         _messageThread.Start();
  16:     }
  17:  
  18:     // passes the interface method as a delegate using method group
  19:     public MessageListener(ISubscriber subscriber, IMessageHandler handler)
  20:     : this(subscriber, handler.OnMessageReceived)
  21:     {
  22:     }
  23:  
  24:     // handle the looping in the thread
  25:     private void MessageLoop()
  26:     {
  27:         while(!_isHalted)
  28:         {
  29:             // as long as processing, wait 1 second for message
  30:             Message msg = _subscriber.Receive(TimeSpan.FromSeconds(1));
  31:             if(msg != null)
  32:             {
  33:                 _handler(msg);
  34:             }
  35:         }
  36:     }
  37: }

}

This is the method I tend to prefer because it allows the user of the class to choose which method works best for them.

You may be curious about the actual performance of these different methods.

   1: Enter iterations:
   2: 1000000
   3:  
   4: Inheritance took 4 ms.
   5: Events took 7 ms.
   6: Interface delegation took 4 ms.
   7: Lambda delegate took 5 ms.

Before you get too caught up in the numbers, however, keep in mind that this is performance over over 1,000,000 iterations. Since they are all < 10 ms which boils down to fractions of a micro-second per iteration so really any of them are a fine choice performance wise. As such, I think the choice of what to do really boils down to what you're trying to do. Here's my guidelines:

  • Inheritance should be used only when defining a collection of related types with implementation specific behaviors, it should not be used as a hook for users to add their own functionality.
  • Events should be used when subscription is optional or multi-cast is desired.
  • Interface delegation should be used when you wish to refer to implementing classes by the interface type or if the type requires several methods to be implemented.
  • Delegate method delegation should be used when you only need to provide one method and do not need to refer to implementers by the interface name.

© Geeks with Blogs or respective owner