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.