Decoupling: Events vs. Dependency Injection

Twitter open sourced Flight: “a lightweight, component-based JavaScript framework from Twitter”. Its killer feature: develop components that are loosely coupled, through the power of events. Cool, right? We all love events.

Events are a powerful idea. They give you a nice way to decouple, and a great way to support extensibility in your software, by letting developers hook in and respond when certain events occur. A good example is the browser’s DOM, which defines many events on HTML elements: onclick, onmouseover etc. As a result, to hook into the DOM you don’t need to subclass it and override the “onClick” method, or patch its source code — you just subscribe to the events you’re interested in and off you go.

The question is: how far do you want to push ‘em?

Although event-based applications are very extensible, their control flow becomes very difficult to oversee. Triggering one event can result in a big storm of new events being triggered, often in unpredictable order, each activating code all over the place. If something doesn’t work the way you expect it to, it’s very hard to debug. I saw this a lot in the implementation of Cloud9 where we used events a lot.

This is why Flight worries me.

Flight seems to be pushing events a bit further by using them to implement loosely-bound-and-possibly-not-bound-at-all method calls.

Two examples from Flight’s README:

this.trigger(“saveRequested”, currentDocument);

The name is phrased as an event: “saveRequested” suggests “hey, just so you know, somebody just requested a save!” However, in the example this event trigger is not followed by a method call that performs the actual save. Instead, the implicit assumption is that some event listener will perform the actual save action. This is how Flight enables decoupling of components: the code that requests the save is now independent from any component that actually performs a save.

The question is: is anybody listening to this event? Will a save actually occur? That will be difficult to say, since no error will be triggered if it doesn’t. If nobody listens to the event, the user’s request to save will go straight to /dev/null.

As I see it, events should be designed to be ignorable. “Somebody moved his mouse over this div.” Awesome, nobody cares, move along!

An event like “saveRequest” isn’t something that should be ever ignored: somebody requested a save, do something about it!

Another example from the README:

this.trigger(‘uiLoadUrl’, { url: $(‘href’) });

This is an even clearer example: this is not a notification, it’s a call to action. “Hey UI component, whoever you are, load this URL!” Again, it’s cool that this code doesn’t have to have any knowledge where or what this UI component is or how it works, but it may as well be yelling into the void, because there’s zero guarantee that anybody is listening, which will result in nothing happening.

The silent failure that results is a serious problem, especially if you develop larger applications.

“So Zef, can we do any better?”

I’m glad you asked, hypothetical reader — indeed we can!

Enter the wonderful world of dependency injection.

If you would have mentioned dependency injection to me a year ago, I would have given you a dirty look. Dependency injection? That sounds an awful lot like something people use in super bloated Java frameworks like Spring. Yuck! “Dependency injection is a solution to a problem that Javascript doesn’t have!” I would have said. But I would have been wrong.

Dependency injection is a better solution to the problem that Flight tries to solve: it provides a structured, safe and fast way to let components communicate while still being loosely coupled.

Just as a flight application, you build up your application from independent components. However, there is one important difference: rather than having implicit assumptions about other components out there — “well, we’ll just assume there’s a component that listens to event takeABreath, otherwise we’re screwed!” — components in the dependency injection context explicitly define their interface.

The term interface does not imply a fully typed Java-style, or worse WSDL-style interface, it can be very simple. In essence it has to specify just two things:

  1. What are the services provided by other components that I depend on?
  2. What are the services that I expose that other components can use?

For instance, in the Flight example, the sample component could depend on a UI component that exposes a loadUrl service, and a storage component that can save things. How these components are implemented is not relevant and can change at any time.

To wire the whole system together, you simply instantiate all your components, they will automatically “find” each other, and off you go.

From this, there’s two behaviors that you will get that a system like Flight won’t give you:

  1. As soon as your application starts it will verify that all required services are implemented by some other component. If not, it will error on you. This is a good thing, because now you know you may have forgotten to load an important component.
  2. As soon as you call a service method that does not exist, or you you did not declare as a dependency, your application will fail as well. This is also good, because this ensures that you did not mistype the service’s name (which, in an event-based system would result in crickets), and that your dependency list is indeed complete in order to make (1) more useful.

So: no more silent failure.

At Cloud9, we developed a dependency injection library for Javascript, named Architect. We primarily used Architect in our node.js back-end, but I’m pretty sure it works in the client as well. Architect allows you to define reusable components, clearly specifying what services it provides and consumes — the fulfillment of these requirements are statically verified once you launch your Architect application.

Here’s a simple Architect component (or “plug-in” in Architect terminology) that exposes an authentication service and implements it using a database service service defined by some other component:

// Plugin interface plugin.consumes = [‘database’]; plugin.provides = [‘auth’];

module.exports = function plugin(options, imports, register){ // “database” is a service this plugin consumes var db = imports.database;

register(null, { // “auth” is a service this plugin provides auth: { users: function(callback) { db.keys(callback); }, authenticate: function(user, pass, callback) { db.get(user, function (u) { if (!(u && u.password === pass)) return callback(); callback(user); }); } } }); };

It may require slightly more ceremony compared to Flight’s components, but I think the pay-offs are definitely worth it.