Apr 22 2008

Event registry

During projects I've worked on which involved coding JavaScript, I've had positive experiences enjoying the event driven nature of the language and some bad, especially after code bases grew larger with events firing left and right, producing side effects which were difficult to manage or track down. During the bad days, a centralized way of managing events was often brought up in conversations with colleagues as a means of managing the issue.

I've long been a fan of the event driven style of programming as a particularly useful alternative promoting loose coupling of system components. Below is the code for a Ruby module which, when included, enables classes to act as event dispatchers. Other classes can subscribe to the events and be notified when an event occurs.

module EventDispatcher
  module ClassMethods
    attr_reader :listeners

    def subscribe(event, &callback)
      @listeners ||= {}
      (@listeners[event] || @listeners[event] = []) << callback
    end

    private

    def clear_listeners!
      @listeners = {}
    end
  end

  def self.included(receiver)
    receiver.extend(ClassMethods)
  end

  def notify(event, *args)
    self.class.listeners[event].each {|callback| callback[*args]}
  end
end

Let's consider an example application where the resource management department of an organization hires recruits and upon admission notifies interested services to take relevant action.

class RM
  include EventDispatcher

  def hire(name)
    notify(:new_recruit, name)
  end
end

What is especially interesting here is how the RM class doesn't need to know anything about any interested listeners. In order to test RM, it suffices to ensure that a notification is sent upon calling the hire method.

def test_rm_notifies_on_new_hire
  service = mock
  service.expects(:new_recruit).with("name")
  RM.subscribe(:new_recruit) {|name| service.new_recruit(name)}
  RM.new.hire("name")
end

Typically, listeners would directly register their interest to the event by subscribing to it. Let's imagine a welcoming letter is issued to new recruits the moment they join the organization.

class WelcomeLetterService
  RM.subscribe(:new_recruit) {|name| greet(name)}

  def self.greet(name)
    "Welcome, #{name}!"
  end
end

The first thing to notice is WelcomeLetterService's direct coupling to RM. Additionally, a code base heavily employing this strategy might suffer ill effects similar to the ones described in the JavaScript inspired first paragraph of this article. Allowing a centralized option for event registration and management could serve as possible remedy to the issue.

module EventRegistry
  def register_event_listeners
    RM.subscribe(:new_recruit) {|name| WelcomeLetterService.greet(name)}
  end

  extend self
end

The EventRegistry module acts as a centralized mechanism for declaring which listeners subscribe to which events. On top of that, the WelcomeLetterService is now completely oblivious to the RM class.

class WelcomeLetterService
  def self.greet(name)
    "Welcome, #{name}!"
  end
end

Due to the level of decoupling achieved, testing these two components becomes particularly easy.

def test_welcome_letter_service_greets_by_name
  assert_equal(WelcomeLetterService.greet("Name"), "Welcome, Name!")
end
 
def test_event_subscription_wiring
  WelcomeLetterService.expects(:greet).with("name")
  RM.expects(:subscribe).with(:new_recruit).yields("name")
  EventRegistry.register_event_listeners
end

All this ties well with the philosophy behind Synthesized Testing, where a coherent collection of lightweight tests becomes a major factor of confidence that the system under test is complete, reducing the need for complex overarching tests.