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.