Decoupling JS - using Classes to wrap behavior
This post is the 1st in a series of posts about decoupling JS code. You can find the table of contents here
Now, let me just clarify – when I say classes – I'm not necessarily talking about Mootools Class – I'm talking about the notion of a package – A piece of code encapsulating all of the behavior of a specific part of our site. I can make many generic examples, but I'll pick one that's easy – an Autocomplete widget. This is something that has a very specific behavior – it listens to an element, it fetches data, and it creates a list of options. We can do this with a long list of functions within a closure (for example a ready state):
function openRequest(){
//fetch the current input and query the request and call the request handler
}
function handleRequest(req){
//handle the request and populate results
}
function populate results(res){
//populate the results into a list and shows it
}
var input = document.querySelector('.autocomplete')
, list = document.createElement('ul'); //this will be our result list
//some code that inserts the list, positions it and hide it until it's used.
input.addEventListener('keyup',openRequest,false);
There are several problems with this – it's harder to maintain, it's harder to test, and it's harder to refactor. Consider this example:
function Autocomplete(el){
this.element = el;
this.bound = { //we want to keep the 'this' attribute inside the events
openReq : this.openRequest.bind(this)
, listClick : this.clickList.bind(this)
};
this.generate();
this.attach();
}
Autocomplete.prototype = {
generate : function generate(){
this.list = document.createElement('ul');
this.positionList();
}
, positionList : function positionList(){
// ......
}
, attach : function attach(){
this.element.addEventListener('keyup',this.bound.openReq,false);
this.list.addEventListener('click',this.bound.listClick,false);
}
, openRequest : function openRequest(e){
// ....
}
, listClick : function listClick(e){
// ....
}
};
var input = document.querySelector('.autocomplete')
, au = new Autocomplete(input);
The object will have a state and an identity. Each method has a specific responsibility. This introduces at least 2 levels of decoupling:
- We can replace a method without changing the rest of our code. Each method is almost completely decoupled from the rest, only sharing state with the object's properties.
- Any part of our application using our object is completely oblivious to the implementation.
The above can probably be said about other OO languages out there, each with its own mechanism for this. But there is a 3rd advantage introduced by the prototypal nature of JS – we can take each method and incorporate it into a completely different context, either by simply attaching it to a different object, or by using apply/call/bind. This is a very strong tool, which can only be used if our code is truly decoupled properly.
It should also go without mention that above code is also much easier to port from one application to another.
Next we will talk about understading and using JS's built in Function construct to better decouple our code.