Decoupling: Events vs. Dependency Injection
By Zef Hemel
- 6 minutes read - 1092 wordsTwitter 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.