Using Eventing to Decouple Applications

posted on 10/01/12 at 07:08:31 pm by Joel Ross

I've been writing an application to monitor Jenkins and update a Delcom traffic light with the current build status. I started out with a straightforward approach and it worked well. At first. But as I decided to expand the application to update icons and show a separate window with the current build status, I quickly realized that this wasn't going to be maintainable long term.

Here's what I was doing to update the build status:

   1: projects.Each(p => p.CurrentStatus = projectStatusService.CheckStatus(p));
   2: var buildStatus = GetCumulativeBuildStatusFrom(projects);
   3: delcomService.UpdateBuildStatusTo(buildStatus);

As I started looking at adding other build monitors, my code was going to start to look like this:

   1: projects.Each(p => p.CurrentStatus = projectStatusService.CheckStatus(p));
   2: var buildStatus = GetCumulativeBuildStatusFrom(projects);
   3: delcomService.UpdateBuildStatusTo(buildStatus);
   4: UpdateIconFor(buildStatus);
   5: if (monitorForm != null) {
   6:   monitorForm.SetBuildStatusTo(buildStatus);
   7: }

Notice that the code that's determining the build status is also now responsible for updating the build indicator. And as I added more and more build indicators, this code would have to be touched over and over.

So, rather than continue down the path and not really liking the direction the code was headed, I decided to add eventing.

Before we get to how the code changes, let's look at what we have to add. First, we need an event, which is really just a class:

   1: public class BuildStatusChanged : IEvent
   2: {
   3:     public BuildStatus Status { get; private set; }
   4:     public BuildStatusChanged(BuildStatus status)
   5:      {
   6:          Status = status;
   7:      }
   8: }

The infrastructure to handle it is pretty straightforward. Just one class:

   1: public static class Eventing
   2: {
   3:     private static readonly IDictionary<Type, List<Delegate>> actions = new Dictionary<Type, List<Delegate>>();
   4:  
   5:     public static void Register<T>(Action<T> callback) where T : IEvent
   6:     {
   7:         if (!actions.ContainsKey(typeof(T)))
   8:         {
   9:             actions.Add(typeof(T), new List<Delegate>());
  10:         }
  11:         actions[typeof(T)].Add(callback);
  12:     }
  13:  
  14:     public static void Unregister<T>(Action<T> callback) where T : IEvent
  15:     {
  16:         if (actions.ContainsKey(typeof(T)))
  17:         {
  18:             var item = actions[typeof (T)].FirstOrDefault(i => i == (Delegate) callback);
  19:             if (item != null)
  20:             {
  21:                 actions[typeof (T)].Remove(item);
  22:             }
  23:         }
  24:     }
  25:  
  26:     public static void Raise<T>(T args) where T : IEvent
  27:     {
  28:         if (actions.ContainsKey(typeof(T)))
  29:         {
  30:             actions[typeof(T)].ForEach(a => a.DynamicInvoke(args));
  31:         }
  32:     }
  33: }

When a class wants to know about an event, it just calls Eventing.Register() passing in a callback. So the DelcomService looks like this now:

   1: public class DelcomService 
   2: {
   3:     public DelcomService() 
   4:     {
   5:         Eventing.Register(ChangeBuildStatus);
   6:     }
   7:     
   8:     public void ChangeBuildStatus(BuildStatusChanged args) 
   9:     {
  10:         // turn the traffic light on
  11:     }
  12: }

This same type of code would then be added to any forms that need to know about the current build status, as well as the main application thread that is managing the icon for the application.

As for the code that is checking the build status? It changes slightly:

   1: projects.Each(p => p.CurrentStatus = projectStatusService.CheckStatus(p));
   2: var buildStatus = GetCumulativeBuildStatusFrom(projects);
   3: Eventing.Raise(new BuildStatusChanged(buildStatus));

This is much better. First, the build monitor no longer knows anything about any of the build indicators. Second, if a new build indicator ever is needed (like for an Ambient Orb), this code doesn't change at all.

Thinking in terms of SOLID, we've removed a responsibility from our build monitor, so it truly only has a single responsibility, and we've met the Open/Closed principal as well, because adding new build indicators doesn't require any changes to the code that monitors the build.

The code that this post is based on is open source on BitBucket. It's not exactly straightforward to use yet, but it does work - I use it every day to monitor our builds at TrackAbout. I'm working to make configuration easier, and once that's done, I'll write up a bit more about it.

I'm going to attempt to use Google+ for comments, so if you have anything to add, please leave a comment over there.

Categories: Development, C#