In the previous posts in this series we’ve done some test-driven development of Backbone Models, Collections, and Views. In this post I’m going to cover another role which I believe should be present in most SPA Backbone apps: Controllers.
Controllers in a Backbone app
There’s nothing in the Backbone.js library for building Controllers. There’s not a
Backbone.Controller for you to extend from. But that doesn’t mean that Controllers don’t have a part to play in your Backbone app. I suspect that the reason we don’t see Backbone.Controller in the library is simply because there isn’t much helpful shared functionality that could be put there. The Controller role is more loosly defined than other more concrete roles such as View or Model and so there’s not a need for a concrete base implementation in the Backbone.js library. However that does not mean that your app wouldn’t benefit from code organized under the Controller role.
No Controller class or prototype
What does a Controller do?
Simply put, a controller mediates between the UI and the rest of your application. In the case of a Backbone app this generally means reacting to events published from your View layer. A controller may respond to these events by updating Model instances, or by making calls to Services (which we’ll get to in a later post in this series).
Doesn’t a View do that in a Backbone app?
In a lot of Backbone applications which don’t have Controllers you see Backbone Views that do a lot of the coordination work I just described. Rather than just acting as a translation layer between the UI and the rest of the application these Fat Backbone Views take on additional responsibilities, often acting as Controllers too. These views tend to be large, unwieldy, and tough to test. Their implementations are hard to read and hard to maintain. They cause these issues because they are violating the Single Responsibility Principle, both acting as an abstraction over the DOM and also implementing application logic. Introducing a Controller helps avoid this situation. Views can remain focussed on their single responsibility - mapping between the UI and the application. Controllers take on the additional responsibilities which would otherwise muddy the View’s role. Additionally, as Controllers aren’t coupled to a DOM the way Views are they are much more amenable to testing in isolation.
Back to our example
Now we’ll see what a typical Controller might look like, using our Card Wall app as an example.
Adding new cards to a card wall
When we left our Cards application in our last post we had a
CardWall model which represented a set of cards. We also had a
NewCardView which allowed a user to enter a new card. What’s missing is something which ties those two elements together. When a user fills out the text for a new card and hits create then a new card should be added to the CardWall. This is exactly the place where a Controller would come into play - bridging between a View and a Collection or Model (or both).
Let’s get going and use tests to drive out that controller.
Once again, we’ll write an initial silly-simple test to get us started. We’ll test that we can create a controller:
1 2 3 4
This will fail because we don’t have that constructor function defined yet. We easily remedy that and get our test back to green:
Simple enough. A function that takes no parameters and return an empty object literal. That’s enough to get us to green.
OK, what should we test next? Well, this controller needs to react to a
NewCardView firing an
create-card event by adding a card to a
CardWall. That’s quite a lot of functionality to bite off in one test but I don’t really see a way to break it down, so let’s just give it a go and see if we can express all of that in a reasonably small step:
1 2 3 4 5 6 7 8 9 10 11 12
There’s a lot going on here. We are creating fake instances of
CardWall and passing them into our controller’s constructor function. Then we simulate a
create-card event by explicitly triggering it on our fake
NewCardView instance. Finally we check that our controller reacted to that
create-card event by calling
addCard on the
CardWall instance it was given during construction.
We created our fake instance of
NewCardView by just taking the base
Backbone.Events module and mixing it into an empty object. That Events module is the same module which is mixed into pretty much every Backbone-derived object (Model, View, Collection, Router, even the Backbone object itself!). Mixing the Events module into our empty object gives it the standard Backbone event methods like
trigger. That’s all we needed our fake instance to be able to do in this case - be an empty object with some event methods mixed in.
So now that we understand what the test is doing let’s get it to pass:
1 2 3 4
Reasonably straight-forward. When the controller is constructed it now immediately registers a handler for any
create-card events from the
NewCardView instance - note that the controller is now being provided the
NewCardView instance (and a
CardWall instance) via the constructor function. The handler for that
create-card event simply calls
addCard on the
That gets our previous test to green, but unfortunately it breaks our initial simple test. That test wasn’t passing any arguments to the controller’s constructor function. That means that
newCardView is undefined, and calling
newCardView.on fails. We could solve this by modifying that initial test to pass in some kind of fake
newCardView. However, looking at that test it is providing no real value now. It was there to get us started TDDing our controller, and now that we’re started we don’t really need that test to stick around. It’s very unlikely to catch any regressions or provide any other value in the future so we’ll just delete that first test, which will bring out full test suite back to green again.
If a test isn’t providing any value then don’t be afraid to delete it.
We now have a controller which reacts to
create-card events by asking the
CardWall to add a card, but it’s not passing any information about the card from the
NewCardView to the
CardWall. Specifically, it’s not passing the text which the user entered into the new card view. To drive out that functionality we could add a new test which describes the additional behaviour. However in this case that test would just be an expanded version of the existing test, so instead we’ll just take our existing test and flesh it out more:
1 2 3 4 5 6 7 8 9 10 11 12
We’ve expanded our
fakeNewCardView to include a stub implementation of the
getText method. This method would be present on a
NewCardView instance. We’ve also extended the event triggering in the test to also include the sender of the event. This is what the actual
NewCardView implemention which we built in the previous post does. Finally we modify our verification step. Instead of just checking that
addCard was called it checks that it is called with a
text parameter equal to the card text returned by the
We run this test and of course it fails, but it’s very simple to get it passing:
1 2 3 4
The only thing we’ve changed here is to take the sender argument passed with the
create-card event, grab the text from that sender and then include it when calling
cardWall.addCard. Tests are green once more. Looking at this implementation you can see why we had to add the extra parameter in the explicit
fakeNewCardView.trigger('create-card',fakeNewCardView) call in our test code. If we didn’t pass the
fakeNewCardView with the event then
sender would be undefined in our controller code, which would cause the test to fail. It’s unfortunate that this level of implementation detail leaked into our test writing, but that implementation detail is part of the
NewCardView public API, so this isn’t too big a concern.
Our Controller is complete
And at this point we’re done. We have a Controller which mediates between our Views and the rest of our app, creating new cards when a view tells it that the user wants a new card.
That’s our Controller?!
Our controller ended up being 3 lines of code. Was that worth it? Why didn’t we just throw that logic into
Well, if we had done that then
NewCardView would now have to know which
CardWall it was associated with (so that it could call
addCard on the right object. That means
NewCardView would now be coupled directly to the
CardWall. That means you’d either need to wire up each
NewCardView with a
CardWall - probably via constructor params or by setting fields - or worse you’d need
CardWall to become a singleton. Both of these are design compromises. It’s better to keep the different parts of your system decoupled if you can.
Also note that if
NewCardView knows about a
CardWall then you are likely to start to have circular dependencies - Collections or Models know about Views, which in turn know about other Collections or Models. Your design becomes quite hard to reason about - you don’t really know what objects are involved in a particular interaction. More coupling also makes things harder to test. If a
NewCardView is directly calling a
CardWall whenever card creation is requested then you need to supply fake versions of
CardWall in a lot of tests. That’s one more piece of complexity in your tests for you to read, understand and maintain. Better to keep things decoupled so that your tests can remain isolated and focussed, along with the rest of your code.