Modules and Callbacks Going Hollywood with MooTools
During the earlier days of the language, JavaScript played a more supporting role in application design. JavaScript was more of an afterthought: a simple way to add some effects to the page or maybe some client-side validation for forms.
The recent “rediscovery” of the language, however, made a huge impact on the way we use JavaScript, and it has become the leading star for many recent applications. Client-side scripting is no longer just about adding simple effects to web pages, but has become a way to implement impressive applications that run in the browser.
But as client-side applications get bigger and bigger, the code that powers them become more and more complex. Parts of the application start getting more dependent on other parts, which introduces spaghetti code filled with strong coupling between items. And the more coupling involved, the harder it is to manage applications: modifying existing functionality becomes a chore, and adding new parts becomes one step short of a nightmare.
Any sufficiently complex application has the risk of getting out of hand unless some sort of modular design was setup from the beginning. Designing your application to be modular isn’t a hard thing to do–although it requires some rethinking if you’re not familiar with the process.
In this post, we’ll examine one way of taming complexity in applications using modules that apply the Hollywood Principle. We’ll be using MooTools here, since it’s my weapon of choice and because MooTools itself provides features that enable us to easily implement modular code. However, you can use the same ideas here with any framework you like, so long as they provide the APIs you’ll need.
So if you’re up for it, let’s take a modular stroll.
Growing Simple Apps
We’ll start by examining an example of a very simple client-side application: an address book. It’s a very simple application consisting of a list of contacts and an area for displaying the contact information. Our example application has the following HTML markup:
<ul id="contacts"> <li data-id="contact-001">Garrick Cheung</li> <li data-id="contact-002">Mark Obcena</li> <li data-id="contact-003">Christoph Pojer</li> <li data-id="contact-004">Jabis Sevon</li> <li data-id="contact-005">Tim Wienk</li> </ul> <div id="info"> </div>
The application has a very simple requirement set:
- The contacts are displayed in a list,
ul#contacts
, which will be populated on the server side. - Each of the list items will be clickable, and clicking a contact will send an
XMLHttpRequest
to the server to retrieve the contact information. - The contact information will be in the form of an HTML string, which will be injected into
div#info
.
With those requirements, we came up with the following code:
window.addEvent('domready', function(){
var request = new Request({
url: '/contacts',
link: 'cancel',
onSuccess: function(){
$('info').empty().set('html', this.response.text);
}
});
var contacts = $('contacts').getElements('li');
contacts.each(function(contact){
contact.addEvent('click', function(){
var id = this.get('data-id');
request.send({data: {id: id}});
});
});
});
We used an event handler for the list items’ click
event to control our application. When a user clicks a contact, we send our request and then update our div. This of course, is example code so it’s a bit “naive” compared to what you’d write in real life. However, it serves well for our example.
After some time though, we thought of adding new features to our application. We realized that we need to add a highlight to the currently selected item on the list, so we’ll need to update our click
handler. We also need to add the new notes functionality into our application for displaying contact notes.
First, we update our HTML to add a div for displaying the notes:
<ul id="contacts"> <li data-id="contact-001">Garrick Cheung</li> <li data-id="contact-002">Mark Obcena</li> <li data-id="contact-003">Christoph Pojer</li> <li data-id="contact-004">Jabis Sevon</li> <li data-id="contact-005">Tim Wienk</li> </ul> <div id="info"> </div> <div id="notes"> </div>
Next, we update our application code, adding in highlighting for contact items and displaying the notes:
window.addEvent('domready', function(){
var notesRequest = new Request({
url: '/notes',
link: 'cancel',
onSuccess: function(){
$('notes').empty().set('html', this.response.text);
}
});
var contactRequest = new Request({
url: '/contacts',
link: 'cancel',
onSuccess: function(){
$('info').empty().set('html', this.response.text);
}
});
var contacts = $('contacts').getElements('li');
contacts.each(function(contact){
contact.addEvent('click', function(){
// highlights
contacts.removeClass('selected');
this.addClass('selected');
// requests
var id = this.get('data-id');
contactRequest.send({data: {id: id}});
notesRequest.send({data: {id: id}});
});
});
});
Suddenly, our click handler for the list items has grown more complex. Not only are we using it to fire our request for contact information, but we also cram our highlight code as well as the note requesting functionality into the click handler. And this is just a “simple” application. Imagine working on code like this for a more complex project with more moving parts.
But the worst thing is that this is not just a hypothetical situation: every now and then you’ll see projects coded like this, with components mangled together into one highly coupled mess.
Problematic Code
There are two main problems with this kind of coding:
The first is object coupling. In our code above for example, our Request
instances are highly coupled with our event handler, and it is the event handler which sends the requests by calling the send
method directly. This is a problem with many complex applications, and you might be writing strongly coupled code without knowing it. The easiest way to check if you have strongly coupled code is to look at your object references: are your functions and event handlers referencing objects other than themselves and calling their methods? If so, then you have strongly coupled code.
Strong coupling makes your code inflexible. If we needed to change request objects, for instance, we’ll have to change all references to our original one. That might sound okay if you just have 2 references, but think about a huge application with more than a hundred. It could get hairy really easily.
The second problem is that there is no separation of concerns. All our functionality is handled by the click
event handler, from changing the style of the list to sending the requests. These are separate operations which don’t really depend on each other, and yet we ram all of them into a single function.
Separation of concerns is an important trait of good code. Parts that don’t concern each other should be separated, and each part should only be concerned with the operations that affect it.
In order to make your applications truly manageable, scalable and flexible, you need to stop writing code like the one above and start thinking about decoupling your code and making concerns separate. And the easiest way to do this is to start making your code modular.
Getting Modular
There are many topics involved when you talk about “code modularity,” but the one we’ll concern ourselves here is the concept of modules. A module is a self-contained component whose main purpose is to encapsulate and group related functionality together. A module can be fully independent and work without depending on other modules, but it’s more common to use separate modules together in order to build an application.
There are many ways to implement modules, but the easiest way–and the one we’ll be discussing here–is through objects. In this model, we’ll use objects to group related functionality together by defining module objects that represent a particular component in our application. This same module model is used in many applications, albeit using different names and APIs.
We’ll start by looking at our previous code and dividing it into logical parts which will become separate modules. Looking at the application, we can see that we have three distinct components: the contacts list, the data viewer and the notes viewer.
The contacts list is the first component, and it’s composed of several smaller parts. First we have the actual list element, and then we have the click handler for the list items, and the code for highlighting the list. Turning this to a module object, we’ll end up with something like this:
var list = {
init: function(){
var self = this;
var list = this.list = $('contacts');
var items = this.items = list.getElements('li');
items.each(function(item){
item.addEvent('click', function(){
self.setHighlight(this)
});
});
},
setHighlight: function(el){
this.items.removeClass('selected');
el.addClass('selected');
return this;
}
};
window.addEvent('domready', list.init.bind(list));
As you see, our list
module is a simple object that has two methods: init
and setHighlight
. The init
method is our setup method, and its job is to set object properties that we’ll use later, as well as to add the event handlers to our list items. The setHighlight
method, on the other hand, is our original item highlighting code, which we now turned into a separate function. You’ll also notice that we hooked up init
method as a handler for DOMReady
, and this moves the event handling outside the module.
Our next component is the info viewer, which controls div#info
where our contact data will be displayed. We’ll also include the actual data fetching code in this component, since fetching and displaying contact data are both related functionality.
var infoViewer = {
init: function(){
this.viewer = $('info');
this.xhr = new Request({
url: '/contacts',
link: 'cancel',
onSuccess: this.displayContact.bind(this)
});
},
requestContact: function(id){
this.xhr.send({data: {id: id}});
return this;
},
displayContact: function(){
this.viewer.empty().set('html', responseText);
return this;
}
};
window.addEvent('domready', infoViewer.init.bind(infoViewer));
Like our list
module, our infoViewer
module also has an init
method, which we also bound to DOMReady
, that’s used for setting up variables and other necessary components. We added 2 methods to our module that relate to contact data: a method called requestContact
, which is used to send the request to fetch the data, and another method called displayContact
, which acts as the success handler for our request and is used to display the data into the div.
Our final component is the notes viewer, which is very similar to our info viewer. Therefore, we could just tweak our infoViewer
module a bit to produce the noteViewer
module:
var noteViewer = {
init: function(){
this.viewer = $('notes');
this.xhr = new Request({
url: '/notes',
link: 'cancel',
onSuccess: this.displayNote.bind(this)
});
},
requestNote: function(id){
this.xhr.send({data: {id: id}});
return this;
},
displayNote: function(){
this.viewer.empty().set('html', responseText);
return this;
}
};
window.addEvent('domready', noteViewer.init.bind(noteViewer));
And with that, we now have our three modules.
You’ll notice three things about our module objects. First, they all share the same basic structure: setup is done via the init
method, which is used as a DOMReady
handler and all methods are defined within the object itself. While the use of the init
method name is a personal choice, these examples showcase the use of a consistent API and structure among modules, which is important in order to establish the fact that each component belong to a specific class of objects (i.e. module objects).
The second thing is that we defined the modules using simple JavaScript objects rather than using classes. There are two reasons for this: first is to bypass having to define and instantiate a class which we’ll use only once–two steps that could be merged by simply using objects directly; and second is so that you won’t get the idea that modules can only be done using classes.
The last thing to notice about our module objects is that we eliminated our two original problems regarding separation of concerns and coupling. Each part of the application is now separate, and each module only concerns itself with the features related to it. We also have no coupling, because all our modules only reference themselves.
However, the decoupling also presents a new problem. Sure, our modules are now decoupled, but they’re also disconnected. Right now, each module is concerned only with itself and that’s great, but our applications need our modules to somehow connect with each other to trigger operations. The infoViewer
and noteViewer
modules in particular need to be able to hook-up to the list
module, so that their request methods could be called to populate their viewers.
So how are we going to connect these separate modules? The answer lies, curiously, in Hollywood.
Module, You Have A Callback
The name Hollywood Principle comes from a very common expression that anyone who’s ever auditioned or applied for anything would be familiar with: “Don’t call us, we’ll call you.” This six words present a very interesting and useful paradigm for programming–one that’s very useful in our current discussion.
At the core of the Hollywood Principle is the idea of relinquishing control over operations. In our earlier code, for example, our list’s click handler controlled the sending of the requests by explicitly calling the send
methods of the request objects. This doesn’t follow the Hollywood Principle because the control of other objects is done by another object.
To apply the Hollywood principle, the control of the operations has to be given up by our click handler. Instead of calling the send
methods of the requests for example, the click handler would only inform the objects that an action has happened. It will be up to the request objects to decide whether they want to react to that action, or ignore it completely.
There are many ways to apply the Hollywood principle in code, like the use of message passing to inform objects about a particular action happening. However, the most common way to do it–and the one that we’re going to use here–is using events and callbacks.
If you think about it, events and callbacks are a great example of relinquished control. Take DOM events, for instance: when a certain action is performed on an element, the element simply fires an event. The element doesn’t really care if any other object is listening, and it has no control over what particular objects will react to the event. The decision of what to do when an event is fired depends on objects–if they want to react to the event, they’ll have to explicitly add a callback using a listener.
Events and callbacks is a great way to connect our modules together, while still maintaining modularity. Instead of modules explicitly calling the methods of other modules, they can simply fire events to which other modules can hook up to using callbacks. There are three main approaches to using events and callbacks for our modules, each with their own pros and cons. And we’ll look at them one by one.
“I Call You, You Call Me”
The first approach is using a pattern called Publish-Subscribe, or PubSub for short. With this pattern, modules broadcast events, together with the data related to that event. Other modules can the listen to these events and use the related event data to perform actions.
In order to use this pattern, we have to transform our modules from simple objects to event-producing objects. Thankfully, MooTools provides us with the Events
mixin class which we can use to easily transform our objects into event-producers. If we were using classes, using Events
is as simple as adding the class to our Implements
member, but since we’re using simple objects, we’re going to use an extension function:
// use Object.append (1.3) or $extend (1.2)
var extend = Object.append || window.$extend;
// the list module
var list = {
init: function(){
var self = this;
var list = this.list = $('contacts');
var items = this.items = list.getElements('li');
items.each(function(item){
item.addEvent('click', function(){
self.setHighlight(this)
});
});
},
setHighlight: function(el){
this.items.removeClass('selected');
el.addClass('selected');
return this;
}
};
// turn the list module into an event-producer
extend(list, new Events);
window.addEvent('domready', list.init.bind(list));
Here, we extended our list
module with an instance of Events
using the extend
function. This gives the module the event-related methods from Events
, such as addEvent
and fireEvent
, which are the two functions we’ll use. Applying the same to all our modules, we’ll get something like this:
var extend = Object.append || window.$extend;
// the list module
var list = {
init: function(){
var self = this;
var list = this.list = $('contacts');
var items = this.items = list.getElements('li');
items.each(function(item){
item.addEvent('click', function(){
self.setHighlight(this)
});
});
},
setHighlight: function(el){
this.items.removeClass('selected');
el.addClass('selected');
return this;
}
};
extend(list, new Events);
window.addEvent('domready', list.init.bind(list));
// the info viewer module
var infoViewer = {
init: function(){
this.viewer = $('info');
this.xhr = new Request({
url: '/contacts',
link: 'cancel',
onSuccess: this.displayContact.bind(this)
});
},
requestContact: function(id){
this.xhr.send({data: {id: id}});
return this;
},
displayContact: function(){
this.viewer.empty().set('html', responseText);
return this;
}
};
extend(infoViewer, new Events);
window.addEvent('domready', infoViewer.init.bind(infoViewer));
// the note viewer module
var noteViewer = {
init: function(){
this.viewer = $('notes');
this.xhr = new Request({
url: '/notes',
link: 'cancel',
onSuccess: this.displayNote.bind(this)
});
},
requestNote: function(id){
this.xhr.send({data: {id: id}});
return this;
},
displayNote: function(){
this.viewer.empty().set('html', responseText);
return this;
}
};
extend(noteViewer, new Events);
window.addEvent('domready', noteViewer.init.bind(noteViewer));
Note that making all modules event-producers isn’t really necessary, because not all of them are going to need to fire events and accept callbacks. However, it is better to make them event-producers from the beginning since you’ll never know if you’ll need them to fire events in the future.
Now that all our modules our event-producers, we’ll need to change them a bit so that they’ll be able to make use of events. The first thing we need to do is to determine how our modules connect to each other. We know that the fetching of the contact info and the note data depend on clicking an item in the list. Therefore, our list
module needs to broadcast an event that will tell other modules that an item has been clicked. This broadcast should also contain some data regarding which particular item has been clicked.
The click
handler for our list
module is in the init
method, so let’s modify that to look like this:
init: function(){
var self = this;
var list = this.list = $('contacts');
var items = this.items = list.getElements('li');
items.each(function(item){
item.addEvent('click', function(){
self.setHighlight(this)
self.fireEvent('list.click', {id: item.get('data-id')});
});
});
},
The important line here is the self.fireEvent
call inside the click
handler. This will fire the list.click
event in the list together with an object argument that contains the value of the data-id
attribute of the clicked list item. The event name list.click
is a convention I personally prefer, but you can use any other event name you want.
Since our init
method is starting to balloon again, we better start cleaning it up. We can separate the click
hander into its own method, and then remove some extra code we no longer need:
var list = {
init: function(){
var list = this.list = $('contacts');
var items = this.items = list.getElements('li');
items.addEvent('click', this.itemClicked.bind(this));
},
itemClicked: function(e){
var item = $(e.target);
this.setHighlight(item);
this.fireEvent('list.click', item.get('data-id'));
},
setHighlight: function(el){
this.items.removeClass('selected');
el.addClass('selected');
return this;
}
};
extend(list, new Events);
window.addEvent('domready', list.init.bind(list));
Our list
module, being the one that fires events, is now designated as the publisher. The other modules, which are going to listen to events, are now going to be designated as subscribers. We can hook up our subscribers to listen to these events using the addEvent
method of our list
module:
var extend = Object.append || window.$extend;
// the list module
var list = {
init: function(){
var list = this.list = $('contacts');
var items = this.items = list.getElements('li');
items.addEvent('click', this.itemClicked.bind(this));
},
itemClicked: function(e){
var item = $(e.target);
this.setHighlight(item);
this.fireEvent('list.click', item.get('data-id'));
},
setHighlight: function(el){
this.items.removeClass('selected');
el.addClass('selected');
return this;
}
};
extend(list, new Events);
window.addEvent('domready', list.init.bind(list));
// the info viewer module
var infoViewer = {
init: function(){
this.viewer = $('info');
this.xhr = new Request({
url: '/contacts',
link: 'cancel',
onSuccess: this.displayContact.bind(this)
});
list.addEvent('list.click', this.requestContact.bind(this));
},
requestContact: function(id){
this.xhr.send({data: {id: id}});
return this;
},
displayContact: function(){
this.viewer.empty().set('html', responseText);
return this;
}
};
extend(infoViewer, new Events);
window.addEvent('domready', infoViewer.init.bind(infoViewer));
// the note viewer module
var noteViewer = {
init: function(){
this.viewer = $('notes');
this.xhr = new Request({
url: '/notes',
link: 'cancel',
onSuccess: this.displayNote.bind(this)
});
list.addEvent('list.click', this.requestNote.bind(this));
},
requestNote: function(id){
this.xhr.send({data: {id: id}});
return this;
},
displayNote: function(){
this.viewer.empty().set('html', responseText);
return this;
}
};
extend(noteViewer, new Events);
window.addEvent('domready', noteViewer.init.bind(noteViewer));
We connected our infoViewer
and listViewer
methods to list
module by attaching event listeners. Now, when an item in the list is clicked, our other modules will be notified of the event, and they’ll be able to act on the event by sending their requests objects to retrieve the data and display it on the interface.
However, you might have noticed one big problem: strong coupling is back. Our infoViewer
and listViewer
modules reference the list
module directly in order to add listeners, and this makes it harder to change the code in the future. If we want to replace our list
module with another one, we’ll have to hunt down all references to our original list
module and replace them.
So while this simple PubSub pattern is useful and easy to implement, the trade-off of having explicit module references is too big to make it usable. Therefore, we need a better alternative.
“I Call You, You Call Them”
In order to circumvent the strong-coupling between the modules, we can implement another pattern related to PubSub called the Observer pattern. In this pattern, the subscriber modules no longer connect directly to the publisher. Instead, an intermediary object called the observer connects to the publisher and the subscribers connect to the observer.
The key to using the Observer pattern is the observer object. There are many ways to implement an observer object, ranging from simple to complex, and the one we’re going to use here is a very basic one that’s built by subclassing Events
.
var Observer = new Class({
Extends: Events,
$publisher: null,
$handlers: {},
initialize: function(publisher){
this.setPublisher(publisher);
},
setPublisher: function(publisher){
this.detach();
this.$publisher = publisher;
this.attach();
},
attach: function(){
var publisher = this.$publisher,
handlers = this.$handlers;
if (!this.$attached && publisher){
for (var event in handlers){
publisher.addEvent(event, handlers[event]);
}
}
this.$attached = true;
return this;
},
detach: function(){
var publisher = this.$publisher,
handlers = this.$handlers;
if (this.$attached && publisher){
for (var event in handlers){
publisher.removeEvent(event, handlers[event]);
}
}
this.$attached = false;
return this;
},
addEvent: function(type, fn){
var self = this,
handlers = this.$handlers;
this.parent(type, fn);
if (!handlers[type]){
var handler = handlers[type] = function(){
self.fireEvent.apply(self, arguments);
};
this.$publisher.addEvent(type, handler);
}
return this;
}
});
As you can see, we subclassed Events
and added a few special methods. The setPublisher
method allows us to change the publisher on the fly, making it possible for us to switch publisher objects without subscribers knowing about it. The attach
and detach
method, on the other hand, manage mass adding and removal of internal handlers of the observer. Finally, we overrode the original addEvent
method of the Events
class to add the necessary logic for adding the internal handlers of our observer.
Instances of the Observer
class act like proxies: they listen to an object for events and then fire the same event for the objects listening to them. Let’s modify our original modules code to use the new Observer
class:
var extend = Object.append || window.$extend;
// the list module
var list = {
init: function(){
var list = this.list = $('contacts');
var items = this.items = list.getElements('li');
items.addEvent('click', this.itemClicked.bind(this));
},
itemClicked: function(e){
var item = $(e.target);
this.setHighlight(item);
this.fireEvent('list.click', item.get('data-id'));
},
setHighlight: function(el){
this.items.removeClass('selected');
el.addClass('selected');
return this;
}
};
extend(list, new Events);
window.addEvent('domready', list.init.bind(list));
// the list observer
var observer = new Observer(list);
// the info viewer module
var infoViewer = {
init: function(){
this.viewer = $('info');
this.xhr = new Request({
url: '/contacts',
link: 'cancel',
onSuccess: this.displayContact.bind(this)
});
observer.addEvent('list.click', this.requestContact.bind(this));
},
requestContact: function(id){
this.xhr.send({data: {id: id}});
return this;
},
displayContact: function(){
this.viewer.empty().set('html', responseText);
return this;
}
};
extend(infoViewer, new Events);
window.addEvent('domready', infoViewer.init.bind(infoViewer));
// the note viewer module
var noteViewer = {
init: function(){
this.viewer = $('notes');
this.xhr = new Request({
url: '/notes',
link: 'cancel',
onSuccess: this.displayNote.bind(this)
});
observer.addEvent('list.click', this.requestNote.bind(this));
},
requestNote: function(id){
this.xhr.send({data: {id: id}});
return this;
},
displayNote: function(){
this.viewer.empty().set('html', responseText);
return this;
}
};
extend(noteViewer, new Events);
window.addEvent('domready', noteViewer.init.bind(noteViewer));
We changed three lines in our previous PubSub code. First, we declared an new Observer
instance called observer
, passing in the list
module as an argument. Then we changed our infoViewer
and noteViewer
code to listen to the observer
object rather than the item
module. What happens now is that every time the item
module fires its list.click
event, the observer
object will also fire the same event with the same arguments to alert the infoViewer
and noteViewer
modules that an item was clicked.
This might not look like a very big step from the PubSub model, since our two subscriber modules still have a direct reference to an object. But the benefit here is that there’s no longer a strong coupling between the actual modules. Because our observer proxies the events, and we can easily switch the publisher object if we want to–making it easy to change the implementations if needed. Now if we need to change our list
module to another module, all we have to do is call observer.setPublisher(anotherModule)
and it’s done. Our subscribers don’t even have to know about the switch.
But while decoupling the publisher modules from the subscriber modules is a great benefit, the direct coupling of the subscriber modules with the observer object is still problematic–especially when you consider the Observer pattern in general.
You see, the Observer pattern implements a one-to-many relationship: a single observer will listen to a single publisher and broadcast to multiple subscribers. In our one publisher application example, this seems easy, since we’ll only have one observer which our subscriber modules will have to know about. But if we start adding more publishers, we’ll need to create observers for each of them and our subscribers will need to keep track of more objects. I don’t know about you, but that doesn’t sound flexible to me.
The Observer pattern solves the problem of coupling between modules, but adds its own coupling problem that doesn’t outweigh the initial benefits. But we’re almost there–we just need to make a few adjustments.
“Tim, Could You Tell Djamil and Chris to Call Val?”
The problem with the PubSub and Observer patterns isn’t really the fact that they couple modules and objects. If we were working on a simple application like our example address book app, there would be no big harm in using these two patterns, since we’re only using a direct reference to a single module–or in the case of the Observer pattern, a single observer. In fact, coupling isn’t really something we can totally eradicate, especially when you consider the use of the window
and document
objects inside objects and classes.
The true problem lies in the number of couplings involved between modules. Imagine an application with several modules, all of them acting as subscribers and publishers. If we use the PubSub pattern, we’ll end up with all modules referencing each other directly. If we use the Observer pattern, we’ll end up with all modules having their own observer and all modules referencing all these observers directly.
As the number of modules grow, the more we’d want to eliminate direct coupling between them, because having all modules bound together is not really better than having a single module that does everything.
So what can we do? Remember that the main limitation with the Observer pattern is that it only implements a one-to-many relationship: one observer for one publisher. But what if the observer implements a many-to-many relationship? We can use a single observer for all our objects, which reduces the number of direct observer-module coupling.
This is in fact another pattern, called Mediator. In this pattern, all modules will talk to each other indirectly using a proxy object called a mediator. The mediator has a many-to-many relationship with modules: all publisher modules will use the mediator to broadcast events, and all subscriber modules will use the mediator to listen to events.
There are many ways to implement a mediator object, but there’s one way that’s already available to us. This is through the use of a global event-producing object, like the window
object. Modules will send messages to the window
object using fireEvent
, and they will listen to the broadcasts using addEvent
.
// the list module
var list = {
init: function(){
var list = this.list = $('contacts');
var items = this.items = list.getElements('li');
items.addEvent('click', this.itemClicked.bind(this));
},
itemClicked: function(e){
var item = $(e.target);
this.setHighlight(item);
window.fireEvent('list.click', item.get('data-id'));
},
setHighlight: function(el){
this.items.removeClass('selected');
el.addClass('selected');
return this;
}
};
window.addEvent('domready', list.init.bind(list));
// the info viewer module
var infoViewer = {
init: function(){
this.viewer = $('info');
this.xhr = new Request({
url: '/contacts',
link: 'cancel',
onSuccess: this.displayContact.bind(this)
});
window.addEvent('list.click', this.requestContact.bind(this));
},
requestContact: function(id){
this.xhr.send({data: {id: id}});
return this;
},
displayContact: function(){
this.viewer.empty().set('html', responseText);
return this;
}
};
window.addEvent('domready', infoViewer.init.bind(infoViewer));
// the note viewer module
var noteViewer = {
init: function(){
this.viewer = $('notes');
this.xhr = new Request({
url: '/notes',
link: 'cancel',
onSuccess: this.displayNote.bind(this)
});
window.addEvent('list.click', this.requestNote.bind(this));
},
requestNote: function(id){
this.xhr.send({data: {id: id}});
return this;
},
displayNote: function(){
this.viewer.empty().set('html', responseText);
return this;
}
};
window.addEvent('domready', noteViewer.init.bind(noteViewer));
You’ll notice some changes between this version of our code and the others. The first one is that we removed the extension code for our modules that add the methods from the Events
class. Since we’re going to use the window
object to talk between modules, there’s no need to make the modules themselves event-producers because the window
object is already an event-producer.
The other thing about this code is that we’re referencing the window
object directly and calling its addEvent
and fireEvent
methods directly from our module code. This makes our modules decoupled from each other in the sense that they no longer need to know about the existence of other modules. All their talk is directly done via the mediator, the window
object.
Of course, using the window
object as the mediator still seems a bit icky, since we still have coupling involved. However, the costs of this coupling between the window object and a module is outweighed by the benefits of having all your modules totally decoupled from each other. You can add as many modules as you’d like that use this Mediator pattern and still get the flexibility you want, because modules are now totally separate from each other. And because the window
object is highly available as well as unswappable (have you ever tried changing the window
object with your own implementation?), you can be assured that you’ll never really have to worry about using it.
Moodularity
We certainly covered a mile today, didn’t we? I hope that after reading this post, you’ll start thinking about modularity in your code, and I hope that I’ve given you enough starting points to make your code modular. If you’re new to the idea of events, callbacks and modules, it might take some time before the patterns I’ve discussed here will sink in, but I assure you that after using them in your code, you’ll find your applications more manageable and flexible.
Personally, I use a mixture of these patterns in my work, and I’ll share this mixture to you in the near future with some new code and another post. For the moment, make sure you post your comments, questions or suggestions in the reactions area below. And don’t forget to subscribe to the RSS Feed to get updates on next posts or to follow me on Twitter.
And with that, let’s start making Hollywood proud!
Comments
Post a Comment
You can no longer comment on this post. If you have something important to say, reach out via Twitter or via email (remove the nospm part).