Backbone Design Pattern, Controller Delegate
The guys over at SupportBee wrote a post (here is the HN submission) about a few of the patterns and conventions they have been following while implementing their app using Backbone. Their post inspired me to write one myself.
Things to Consider
One of the over simplifications made by some is that Backbone is simply an MVC stack, so one should use it as such. Although true, this doesn't help or infer anything about the implementation of an app using Backbone. I will be using an event-driven approach to explain this particular pattern.
Backbone has native support for Models and Views, but nothing called a Controller. So why is it considered an MVC stack (the Backbone FAQ doesn't actually consider it to be an MVC in the traditional sense)? Don't fear, a controller is relatively subjective in it's purpose and implementation. After reading, whiteboarding and evaluating, I have come to a relatively simple, yet abstract convention for defining a controller in Backbone. Well.. kind of.
One component of an application I am working on has three different models. Each model has a relationship with each other model is some way. Complexity in event-driven programming comes when one tries to consider all possible scenarios for invocation of events for all models.. all at once. Each model, of course, has a corresponding view class since they are all represented in the DOM. Now, here is where things can get a bit ambiguous. Backbone has this notion of a Collection. A collection is simply an ordered set of models that provides a couple convenient hooks for managing, fetching and filtering the models. Well.. so what? It sounds like a glorified array to me.
All that aside, there is a higher purpose a collection can serve and how we can solve the event-driven complexity woes. It can act as a controller for it's models.. or more specifically a delegate to the models which improves the separation of concerns in your app and overall simplifies it. Likewise, I more or less formalized the notion of a CollectionView in my app as well. Keeping a pure event-driven approach with a central source of truth e.g. AppState, ensures the event trigger path is deterministic.
Here is some (incomplete) code for the domain data model (written in CoffeeScript):
AppState = new Backbone.Model domain: null class Domain extends Backbone.Model class DomainCollection extends Backbone.Collection model: Domain initialize: -> AppState.bind 'change:domain', @changeDomain # delegator/controller responsibilities.. changeDomain: (state, model, options) -> if (previous = state.previous('domain')) previous.trigger 'deactivate' model.trigger 'activate' class DomainView extends Backbone.View events: 'click': 'click' initialize: -> @model.bind 'activate', @activate @model.bind 'deactivate', @deactivate activate: => $(@el).addClass 'active' deactivate: => $(@el).removeClass 'active' click: -> AppState.set 'domain', @model class DomainCollectionView extends Backbone.View el: '#domains' initialize: -> # some bind to listeners to the collection it represents.. @collection.bind 'reset', @reset add: (collection, model, options) => view = new DomainView model: model $(@el).append view.render().el reset: (collection, model, options) => collection.each @add domains = new DomainCollection $ -> new DomainCollectionView collection: domains domains.fetch()
Although this only represents a single model, you can see how the Domain model and view mind their own business and do what they need to do in order to reflect the current state of the app with respect to themselves. The
DomainCollectionView handles delegating to the domains and views being affected.
Also notice the click handler on the
DomainView sets the
AppState's domain property rather than attempting to manage triggering state changes itself. This is a common mistake when starting out with event-driven programming and dealing with multiple accepting states. An end-user click is merely a trigger for a state change and should be treated as such. The actualy UI changes will happen at a later stage.
An idea I kicked around previous to this pattern was to not have a controller (or delegate). This required having a condition in every event handler to test whether the current domain was the domain being evaluated. This also meant that N event handlers would have to fire where N is the number of models in the collection since every model was independely listening for a change on the
AppState. This obviously was must less performant than the current solution where a model is only delegated to when necessary.
As Jeremy Ashkenas (one of the authors of Backbone) pointed out, there is no single gospel truth. This is merely an approach that has worked for me for this particular use case. I intend to evolve this post as I see necessary to better explain, add more code or to refine the diagrams. The diagrams were created using Linowski Interactive Sketching Notation.