Rebuilding My Backbone.js Plugins With Modules, SRP and More
A Big Ball Of Mud
Filtering The Mud
People were requesting functionality that my code considered an “edge case” originally, but was in reality, something very useful. One such example is the “named states” request for my memento plugin. A user (and myself, honestly) want to be able to store state with a given name, so that the model can always be rolled back to it, by name. This is a feature that I still need to implement, mind you, but prior to refactoring the code for this plugin, I wouldn’t have been able to do it without serious headache.
I also saw bug reports that I couldn’t fix because there was no clear way to get to the information that I needed, at the time I needed it. For example, my modelbinding plugin would set up HTML DOM events and modify the model when the DOM elements changed. However, the ‘unbind’ process was unable to handle the DOM event bindings (I’ll show you the detail in the “instances vs literals” section, later). After refactoring the code, though, the inclusion of this functionality was trivial – it was obvious that it should have been there, and it took only minutes to implement it.
The Single Responsibility Principle
If you’ve paid attention to my blog for more than a few months, talked with me at a conference, or watched any of my presentations, you’ve probably heard me drone on and on about the Single Responsibility Principle (SRP). It’s an important one, in my opinion. And yes, it applies to pretty much any language you code in – not just object oriented languages.
In this particular case, my memento plugin was in serious violation of SRP. I was originally very proud of myself for having written a very well encapsulated object to handle all of the memento’ing process. But encapsulation is not the only vector on which we judge the quality of code, and I failed miserably in the responsibility department.
As it turns out, the solution I created contains far more than just one responsibility. Here’s a small list of things that this code wraps up:
- Create a memento (serialize the state of a model or collection)
- Store the mementos (push on to a stack: first in, last out)
- Retrieve the previous memento from storage (pop it off the stack)
- Restore the model or collection’s state (deserialize)
- Rollback all the way to the beginning of the model’s saved states (if there is one)
This list isn’t too bad. But when you have all of this functionality wrapped up in only a handful of methods, all within the same object, it turns into a giant mess that is very difficult to read and understand. (If you’d like to take a moment to poke your eyes out, you can read the code that I am talking about, here. Can you even find the public API in that mess?)
Let’s take a different look at the list of functionality, now, and pull things into a logical grouping based on the work that each of the above functions does:
- A High level workflow: organize all of the parts, to make this thing function
- A stack
- Push a memento
- Pop a memento
- Roll back to the first memento
- A serializer
Suddenly the code starts to make more sense. The difference pieces of the puzzle start to fall in place, and we can start to compose the system out of many small pieces that do one thing well, and have one reason to change.
The end result of this refactoring can be summarized with the high level Memento class: the driver of all the workflow in this plugin:
It’s easy to see what’s happening here. You don’t need to pick apart the guts of implementation in order to understand the high level process because this is the high level process. If you want to know the detail, you can dive down into it when you need to instead of being forced to wade through it constantly.
Object Instances vs Object Literals And Functions
Probably the single largest mistake I made when building the modelbinding plugin was to use object literals and singleton objects throughout the code. I thought I was being clever, at first. The problem with “clever”, though, is that you’ll rarely find it coinciding with “intelligent” and “elegant” once you get past the initial implementation and need to add features and functionality.
You can add any amount of functionality that you want to an object literal, with the exception of not being able to create “private” variables (and I put “private” in quotes because it’s only private by the use of closures, not by a syntactical keyword called “private”). In order to create “private” variables, you have to enclose the object literal in another function to build the closure around the variable.
This is often called the “revealing module” pattern (or something along those lines, I think) as it allows you to reveal an API that you want without having to expose all of the internals of your module. Object literals and modules have their usefulness, mind you. I use them frequently and recommend you read up on them. But like any other tool we have, these can be used and abused, and my modelbinding plugin was beating these patterns into the ground.
It all came to a head when I got the bug report saying my code was no unbinding the DOM element events. After digging into this, I realized that because I was using object literals everywhere, I had no way of storing the configuration data I needed for any given Backbone view and model. Without this configuration, there was no way for me to know which HTML elements had which DOM events firing for which models. I was stuck with event bindings that I couldn’t remove, without resorting to jQuery calls that would remove all bindings (not what I wanted).
After some work (4 or 5 hours, total), I was finally able to remove the offending object literals from the modelbinding plugin, and switch over to object instances. Now, every call to the `bind` function of the plugin creates a new instance of a `ModelBinder` object. This object stores all of the configuration that it needs for the one specific view and model that it deals with. Then, when `unbind` is called, I don’t need to try and re-calculate the configuration. All I need to do is loop through the list of events configurations and call `unbind` on each of them individually. This worked for both the model and DOM element bindings, making it even easier.
(To see this code in action, go here)
There was another problem that I ran into with the modelbinding code, related to how I stored and retrieved the configuration for each ModelBinder instance.
In the previous version of the code, I recalculated everything when `unbind` was called. This obviously didn’t work, and I wanted to use a ModelBinder instance to store the configuration for me in the new version. However, I also wanted to keep the existing public API for my plugin: `Backbone.ModelBinding.bind(view)` and `Backbone.ModelBinding.unbind(view)`. This API relied on singleton object methods, instead of object instance methods – back to square one with the object literals problem, right?
Since the configuration of a given ModelBinder instance was for a specific View instance, it makes sense for these two items to be stored together – to use the View instance as a way to store and retrieve the ModelBinder. At first, I was going to use another object to store the View: ModelBinder as a key/value pair. But then I realized that I didn’t need to do this, either. Since the View itself is passed into both the `bind` and `unbind` functions, I can store the ModelBinder directly on the view.
Then, when I call `unbind` and pass in the view, I only need to check for the existence of a `modelBinder` attribute on the view. If it exists, I can assume that it’s my ModelBinder class with all of the configuration for that view, and have it do all of the unbinding for me.
And So Much More …
There’s so much more that went into the refactoring and re-building of these plugins, still. I used techniques such as init-time branching instead of run-time branch. I wrapped my code in modules, providing a clean public API while hiding the internals of my code away from the outside world. And I relied on my test suite, written in Jasmine for both plugins, to tell me when I was breaking the functionality of the system and when I was putting it all back together, resulting in only 1 minor breaking changing to the public API in my modelbinding plugin (which I purposefully did, btw).
A View Into My Thoughts And Process