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: $(e.target).attr('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.

Got something to say?
  1. C# has a concept of Delegates that, at first look seems like a clumsy way of allowing “function pointers” to be passed around. A clumsy wrapper of sort, that allows mixing various callable types in a callable collection that in otherwise type-safe system.

    The reason I bring this up is that word “collection,” which bring me back to JavaScript and Flight

    There is one major difference between the use patterns of “Dependency Injection” and “PubSub”

    While in Dep Injection you expressly prescribe a handler type, in PubSub you can have None, One or Many handlers called without the caller being burdened with looping over the handlers.

    While I get the feel-safe aspect of what is essentially a type-safe anachronism – dependency injection, what I realized long time ago is that the freedom to mix and match observers is somehow more dear to me than tight coupling.

    Example:

    Two separate and completely independent listeners can be listening on “saveRequested”. One displays a message. The other actually does the save. You may have a third one introduced in “debug” mode that logs the details of what the “saveRequested” passed around.

    So, how do you know if something had handled the “saveRequested”? Listen on “saveHandled” channel.

    In your case you are confusing the habits and conventions of type-safe coding with business logic. The need to know that your file was saved is “business logic.” “Finality” or “Completeness” of file save is what you are looking for, not that the call has a handler. Once you concentrate your mind on what you need to ACCOMPLISH as opposed to do I have a handler for it you will see that Dep Injection is a false promise.

Trackbacks for this post

  1. Fixing Events in Javascript - Zef.me
  2. Writing less code for a given deliverable – is that possible? « Supercoderz

Comments are closed now.