the more i use backbone.js, the more i love it. probably because the more I use it the better I get at real javascript, not a framework. — Johnny Megahan (@johnmegahan) August 19, 2012

I saw Jeremy Ashkenas retweet this and it really struck a chord with me. I started playing with Backbone about a year ago and have since used it on large and small projects at work and for fun.

These last two weeks I have been refactoring some JavaScript on StickyGram and I was really surprised at the state of the code I had written not all that long ago. I have been rewriting it to use a number of design patterns that I have (somewhat) inadvertently picked up from Backbone (and underscore).

One object one responsibility

We all write clean encapsulated object oriented JavaScript right? Guys? No me neither – it’s so easy to just fire off an event, nest a few levels of callbacks and be done with it.

Here’s an example. We get some data via an xhr request, append it with a fade effect and then remove some other element.

$ .ajax({ url : ' /wherever ' , success : function (data, status, xhr) { $ ( ' selector ' ).append( $ (data).hide().fadeIn( function (){ $ ( ' other-selector ' ).remove(); })); } });

This is simple to write initially, but it becomes very tricky to extend. Say we also want to update another part of the document with a notice to say that the request was successful:

$ .ajax({ url : ' /wherever ' , success : function (data, status, xhr) { $ ( ' selector ' ).append( $ (data).hide().fadeIn( function (){ $ ( ' other-selector ' ).remove(); })); $ ( ' yet-another-selector ' ).text( ' Request successful ' ); } });

That success callback is gradually getting to be responsible for a lot of different things and is starting to get pretty tricky to read. This only gets worse as other developers come to the code and just need to add a small bit of extra functionality. I find myself cursing them later for the messy code when actually it’s my fault for writing it this way in the first place.

Here’s a much nicer way to deal with this:

function DataModel () { this .url = ' /wherever ' ; } DataModel.prototype. getData = function () { $ .ajax({ url : this .url, context : this , success : this .onSuccess }); }; DataModel.prototype. onSuccess = function (data) { $ (window).trigger( ' DataModel:success ' , data); }; var dataModel = new DataModel();

Now we have a dataModel object which is concerned only with making the request. It triggers an event when it is done which we can listen for in other objects.

function ListView (el) { this . $el = $ (el); this .bindEvents(); } ListView.prototype. bindEvents = function () { $ (window).on( ' DataModel:success ' , $ .proxy( this .addData, this )); }; ListView.prototype. addData = function (data) { this . $el .append( $ (data).hide().fadeIn( this .removeOtherThing)); }; ListView.prototype. removeOtherThing = function (data) { this . $el .find( ' other-selector ' ).remove(); }; var listView = new ListView( ' selector ' );

Right now we have more lines of code, but what we also have is two separate objects each performing a discrete function. Each object has a number of methods and the methods each do one thing. It’s worth noting that we could easily write unit tests for this code – try doing that for the first example…

So now when we need to add another callback to the AJAX completion all we need to do is create a new object type which will listen for the same DataModel:success event and do its thing independently of our listView . That’s pretty awesome and it means that our original code can stay responsible for the one task it was designed.

Thanks, Backbone!

This example follows the same Model/View pattern as Backbone without actually using the library. Actually it’s just well structured code but it’s often easy to take the (seemingly) quicker option.

I’m spending more time up front thinking about the structure and maintainability of my code in the expectation that it will prevent it from getting to that tangled spaghetti-mess that we all hate coming back to and that is definitely a good thing.

Please enable JavaScript to view comments.