Decouple with the observer pattern

An oft-forgotten pattern in C# is the observer pattern. The observer pattern is great for taking a step towards future-proofing your codebase by adding hooks at important spots in the lifecycle of your code. Imagine adding Slack notifications, Data Dog or other functionality without having to modify the core of your application. This article focuses helping library authors rather than a dev who might have instant access to iterate code changes. A library author often can't fit everyone's needs all at the same time. The observer pattern does not replace things like dependency injection (DI). Rather it complements DI rather nicely to cover scenario's that cannot be dreamt up at design time. It also serves well to allow an app to jump into a pipeline and make custom decisions. What follows is a basic run-thru of the observer pattern in C#.

Setting the stage

Let's start with the problem that the observer pattern helps us solve. Imaging you've just created a portable class library. For the purpose of this discussion, your app library deals with e-commerce. During an e-commerce lifecycle key moments in your code like the following list will take place:

  • An item added to cart
  • An item removed from cart
  • The QTY was changed for an item in a customers cart
  • A user checkout has begun
  • A user checkout was successful
  • A user checkout was unsuccessful
  • An exception occurred (payment gateway timed-out or whatever)

Let's focus on just one part of the process, the checkout complete portion. At first stab, it's likely easiest to just add logging code or Slack notification code right into the class itself like this pseudo code below: 

using Slack;
using Logging;

public class MyClass
{
    private ISlack _slack;
    private ILog _logger;

    //constructors, props and whatnot omitted

    public void CheckoutComplete()
    {
        //checkout complete code here

        _logger.LogSomething("User has checked out.");
        _slack.SlackSomething("Hey the user actually bought something!", someChannel);
    }
}

To be sure there is nothing wrong with this approach but we could make some observations (no pun intended):

  • What if we stop using Slack or whatever logger we've selected?
  • What if we need yet-another-notification such as HipChat?
  • What if we simply just wanted to change the text that was output?
  • What if we needed to run some special code at a particular moment?

If we needed to do any of the above, the traditional tasks required would be to iterate thru the changes like so:

  1. Update the code
  2. Recompile
  3. Redistribute it to whomever wants the updated code

It would be much nicer if we could skip all of those steps. Turns out we can but it requires a little bit of engineering to pull it off.

Observe and react

So the observer pattern pushes out the hard dependencies to only the classes that are concerned leaving our core class to deal with only the business items. The observer pattern relies on events that are published and subscribed to by onlookers. We call the classes concerned with the raised events observers. Classes that can be observed by observers are observable which is exactly what we'll do to the MyClass class. The new approach will take the following form:

  • Remove direct references to ISlack and ILog
  • Create an interface that defines the events
  • Implement the events into the observable class
  • Register event handlers in one or more observer classes
  • Register one or more observer classes with the observable class

In our observer class, we will put our hard dependencies to ISlack, ILog or whatever we'd like. The final code will allow us to subscribe to these events and the core library won't know or care about who is observing it nor care what dependencies they need.

Our MyClass class will end up looking like this when we're done:

//no more refs here to Slack or logging

//we have to implement an interface (more on this later)
public class MyClass : ICommerceEvents 
{
    //no more private vars

    //constructors, props and whatnot omitted

    //this is the entire implementation of an event defined in ICommerceEvents
    public event CheckoutCompleteHandler(object sender, CheckoutEventArgs e) OnCheckoutComplete;

    public void CheckoutComplete()
    {
        //checkout complete code here

        //no more explicit logging

        //a variable we'd like observers to have access to
        var grandTotal = 99.99;

        //raise the event we implemented
        OnCheckoutComplete?.Invoke(this, new CheckoutEventArgs(grandTotal); //optionally uses a C# 6.0 null propagation feature with the invoke .? syntax
    }
}

So as you can see, we lose the need for directly referencing Slack, Log4Net, etc in our business logic core class. We also adhere to the open/close principal meaning someone can extend this class by subscribing to OnCheckoutComplete and never has to directly edit this class. The event we raise sends a reference to the caller, which in this case is an instance of MyClass and a set of contextual information that the observer will receive via the CheckoutEventArgs variable.

Before we subscribe, let's show the interface that goes along with this new methodology:

 

namespace MyNamespace {
    //declare what the observing handler should be expecting to receive
    //take note that this goes OUTSIDE of the interface
    public delegate void CheckoutCompleteHandler(object sender, CheckoutEventArgs e);

    public interface ICommerceEvents 
    {
        //define the actual event we can raise
        //this will be implemented in the observable class (MyClass)
        event CheckoutCompleteHandler OnCheckoutComplete;
    }
}

Ok, so now we have a clean MyClass and an interface that defines a key moment in the lifecycle of MyClass (OnCheckoutComplete). One thing we haven't yet addressed is the CheckoutEventArgs class. This class will pass along some contextual information to the observer. It is totally up to you what you want to pass along. For the purposes of this example, let's simply pass along a primitive value for the GrandTotal. Of course you'd likely want to pass along information such as what was in the cart per perhaps, the user ID, etc). The point is you will send information relevant to the event to whomever subscribes. That class would look like the following: 

//inheriting from a .NET base class
public class CheckoutEventArgs : EventArgs
{
    public decimal GrandTotal { get; private set;} //readonly for safety

    public CheckoutEventArgs(decimal grandTotal)
    {
        GrandTotal = grandTotal;
    }
}

Now everything is wired up for a subscriber to come along and listen to the events being raise. So now we need to add the Slack and logging functions but we'll do it as an observer that will subscribe to that particular event: 

using Slack;
using Logging;

public class ObservingMyClass
{
    private ISlack _slack;
    private ILog _logger;

    public ObservingMyClass(MyClass observableClass)
    {
        observableClass += _sample_Delegate;
    }

    private void _sampleDelegate(object sender, CheckoutEventArgs e)
    {
        _logger.LogSomething("User has checked out with a total of: $" + e.GrandTotal);
        _slack.SlackSomething("Hey the user actually bought something totalling: $" + e.GrandTotal, someChannel);
    } 
}

Notice how the subscriber takes a parameter of MyClass. The reason is because that is where we implemented ICommerceEvents. It also means that our instance to MyClass must be the same instance that we raise the event on. To avoid having to have the same instance, you can implement the interface on a global contextual variable that gets passed into MyClass. It would be on that object that the event is raise then. You can also set the event to static and it'll work off of the class static ref instead of the instance.

Register your observer

Your observer won't respond if an instance is never created. Be sure to create an instance of each of your observers and tuck them away for the lifetime of your application either in a global context variable, static ref or a Singleton lifecycle if using DI.

If you wanted to, you could split the Slack method from the logging method by creating another observer, registering both event handlers. Please take note that you have little control over the order observers will fire. As a result, do not rely on order. Also note that event handlers are blocking (synchronous) in nature. Don't let that scare you, most of your code synchronous and blocking by nature. If needed you can setup async event handlers which are out of scope for this discussion.

Summary

If you distribute libraries via Nuget (or wherever) and would like to give extension points to the developers to the dev's that use your lib, try the observer pattern in a few key places in your app. This way your lib doesn't have to try to guess what they may need in the future. The hooks at the key moments are already there.

While I admit most applications have some sort of messaging\logging built-in and they could be swapped with a DI container, but the observer pattern is great for altering the default behavior of a class without having access to the source.

Just like anything, the observer pattern can be over-used and abused. Events could end up firing quite often and the logic tucked into a handler needs to be succinct and fast. Happy coding!