But frameworks are not categorically superior. The browser is a viable host for a wild diversity of applications, and the paradigm of a single framework is not likely suited to all problems. Not to mention, climbing the learning curve and discovering framework conventions is often time-consuming and frustrating.
An elegant weapon for a more civilized age
Wonderfully small and unopinionated, Backbone is perfect for a particular class of problem: the multipage app.
In a traditional web app, the client issues a request and the server returns static HTML content. Each request requires an entire page reload. In a single page app, most HTML rendering is handled client-side (i.e. within the browser), and the server is primarily an API for reading and persisting data.
In pursuit of the practical
At Foraker, we find the multipage app approach practical. Creating a rich client-side experience is almost always costlier than the traditional alternative, even with the benefit of a framework. For instance, vanilla form-based CRUD functionality is trivial to build traditionally with Ruby on Rails, and the CRUD experience is not significantly diminished by full page reloads.
Building multipage apps allow us to be judicious about where we want to spend time (and money) providing compelling interactivity, and where we think traditional request/response architecture will suffice.
Backbone as a foundation
Opting for microapp development via backbone.js instead of utilizing a framework implies that the developer is responsible for establishing sensible organizational patterns and abstractions. Backbone intentionally limits the number and complexity of its abstractions, and we’re expected to fill in the gaps. From the docs:
Backbone.js aims to provide the common foundation that data-rich web applications with ambitious interfaces require — while very deliberately avoiding painting you into a corner by making any decisions that you’re better equipped to make yourself.
Attempting to cram an entire application into Backbone models and views is a common pitfall for novice backbone.js developers. Today we will examine strategies for building upon the Backbone foundation, and formulate abstractions to more effectively structure our applications.
example illustrative archetypal application
Let’s establish requirements for a thesaurus application to help illustrate organizational patterns.
The thesaurus application will help users appear smarter on resumes. There are just two features:
- A search box for finding words and displaying synonyms. We’ll call this the “thesaurus” microapp.
- A text area where users can paste dull resumes. The resume text will be processed, and small words will be replaced with larger, smarter-sounding words. We’ll call this the “resume ensmartening” microapp.
Each microapp gets its own directory and
module.coffee file. This may seem like a great deal of ceremony for the simplicity of our feature set, but this exercise is all about absorbing future complexity and changes.
The anatomy of a module
Each microapp lives in its own module, completely isolated from all other microapps. We’ll use a rudimentary object-literal based module system.
Here is the thesaurus module in
And here is the resume ensmartening module in
The object literal module system allows us to register objects within a namespace:
And access the objects later:
All objects related to the
Thesaurus microapp will live somewhere in the
Thesaurus namespace, and likewise, all objects related to the
ResumeEnsmartener microapp will live in the
We have to go deeper
You’ve probably noticed the
ResumeEnsmartener.App classes. Before discussing these high-level
App classes, let’s switch gears and momentarily dive into the
Thesaurus.Views namespace, working our way back out to the
Right now, the
Thesaurus module contains a single Backbone view:
SearchView accepts user input (e.g. ‘happy’) and returns synonyms (e.g. ‘ecstatic’).
A view in distress
Views are often the first place growing Backbone apps go awry. They slowly accrete domain logic, manage increasingly complex state, persist data, and are unpleasant to test.
Thesaurus.Views.SearchView is an example of a view in distress. It has suffered several requirements shifts and acquired many responsibilities.
Ugh. That wasn't enjoyable.
What does this view do?
- Validates the search query length
- Refreshes search results when the input changes
- Updates the URL state
- Emits analytics data
What should a view do?
Contrary to the design decisions evinced in
SearchView, views have a pretty narrow job description:
A view should only serve to abstract away the DOM.
But what does it mean to “only abstract away the DOM”? Practically speaking, this restriction suggests a view can do two things.
1. Translate DOM activity in to semantic expressions of user intention.
The DOM is an inconsistent mess of classes, IDs and events. Some DOM entities emit “change” events, others “click” events, and through the miracle of jQuery plugins, “sort” or “drop” events. Views serve to hide the mess.
For example, a User might indicate a desire to edit a comment resource by clicking a link with the class “edit”. We identify a click via a “click” event, and the we identify the edit link by targeting the “edit” class.
The view should translate this DOM click event into a user intention by triggering a semantic
"comment:edit" event. The rest of the application just needs to know about this “intention to edit” event, and not the gory link-clicking details.
2. Represent data as HTML.
The view should serialize data into an HTML representation. For example,
SearchView represents search results as
<li> elements within a
<ul> element. Effectively, the view renders a human parsable veneer around data.
Here’s a refactored version of
SearchView which is consistent with the above principles.
SearchView has been reduced to one simple task: communicate the user’s intention to search. It does not validate input, it does not perform the search, it does not emit analytics or update the URL state.
So where did all our logic go? If the view doesn’t execute the search or track analytics, what does?
Once DOM events are translated into user intentions, a controller acts on user intentions. Let’s introduce a controller for the
The controller has taken over the query length validation, URL updating and analytics responsibilities, but engenders new questions. Where did the injected options come from? And where are we handling displaying the search results? Our refactor isn’t much of an improvement if we’ve thrown away functionality…
The answers lie in
App is charged with building dependencies and instantiating the models, controllers and views with their respective dependencies.
Thesaurus.App isn’t much fun; it’s rather dense. Luckily, we needn’t read through too carefully. The
App is almost more of a configuration, wiring together infrastructural pieces with very little behavior of its own.
Leveraging the data layer to gain independence
We can now answer the question of “what happened to rendering search results?”. Rendering of results is extracted into its own
ResultsController. The controller listens for “add” or “remove” events on the results collection and tells the view to rerender, and is never aware of the search process. The tasks of collecting a search query and rendering results are now independent.
Separating searching (i.e. the generation of results data) from results rendering is an example of leveraging the data layer, a key technique for pulling apart a convoluted Backbone application. The data layer is a single source of truth, and is shared amongst all controllers and views. Data updated in one controller is displayed by another, without either controller having to know about the other.
A quick review
To review, a microapp is comprised of the following components, or layers:
- Modules e.g.
- Provide a namespace for the microapp.
- Instantiate collections, models and views
- Inject dependencies and wire objects together at a high level
- Mediate between user intentions and the data layer
- Wrap UI and translate DOM events into user intentions
- Represent data as HTML
- Backbone collections and models which persist application state
- A ‘single source of truth’ shared by many controllers and views
The following diagram demonstrates how the components fit together:
But why structure an application this way?
Layers are important
We should endeavor to stratify applications into layers and define strict boundaries between the layers. Layers accommodate decoupling, and decoupling accommodates resilience to change and complexity.
Follow the layered architecture rules:
1. Lower layers do not know about higher layers
Our view layer does not need to know about the controller layer, or, really, the application in general. It simply translates DOM activity into user intention. Precisely how user intention is interpreted is beyond the view layer’s purview.
Similarly, the data has no notion of its use, and certainly has no idea about its representation in the DOM as HTML.
2. Lower layers are robust, complete abstractions
Our controller must know about the lower view layer, but it does not need to know about the DOM details. The view layer should be a robust abstraction over the DOM, demanding no knowledge of the DOM from higher layers.
Layered architecture Pros
Simplicity and Reuse
One of the biggest wins of a layered architecture is the simplicity of the layers themselves. The
SearchView once handled the complexities of the DOM, as well as acquiring data, emitting analytics and updating the URL. By restricting the view layer to a limited scope of responsibility, we cured the
SearchView of domain knowledge.
Removing domain logic from the view layer has the happy side effect of greatly increasing the generality of views. Decoupling discovery of the user’s intention to search from how we act upon that intention allowed us to collapse
At this point, there isn’t anything particularly special about
SearchView, giving us the flexibility to create a rather generic
SearchInput class, which could be used elsewhere.
Testability Pro 1: View tests
When domain logic resides in the view, one must exercise the DOM in order to test domain logic. This often puts developers in the unfortunate position of crafting a DOM structure which is a reasonable facsimile of the production DOM structure, and then asserting that data changes when UI is manipulated.
For instance, in order to test the original
SearchView, we would have to create the view’s element within the test DOM, manipulate the search input, stub out the AJAX request for search results, and assert that the results appeared correctly. The undertaking grows increasingly onerous as we introduce additional domain logic like the minimum number of characters to qualify for autosearching.
By isolating the view layer from domain logic, the testing challenge becomes trivial: Manipulate the DOM and assert that the correct semantic event was triggered by the view. That is, assert that the user’s intention was properly expressed.
Testability Pro 2: Controller tests
Testing domain logic in the controller is also simplified. As controllers expect their collaborators to be injected, we can inject stubbed collaborators. For example, a view collaborator must only support event triggering, and does not need to be a real Backbone view.
Applications grow and evolve. New requirements will emerge. Significant mechanical changes may be in order.
Layers grant us flexibility. We can completely supplant our UI without altering the core logic of the application. We can change inputs to sliders or change tables to graphs with ease. We can replace localStorage with server persistence without changing an ounce of view code.
Layered architecture Cons
Ceremony and Indirection
Separating simple applications into layers and distributing responsibilities demands diligence. Introducing new abstractions makes code more, well… abstract. The resulting layered architecture is initially harder to understand than longish procedural code packed into a Backbone view.
Below a certain threshold of complexity, it may be advisable to just stick with models and views.
Building on top of the backbone.js foundation empowers developers to make important design decisions. Unfortunately, as I can relate from personal experience, some design instincts are misguided, and too much freedom can be a detriment. If you’re worried about making good design decisions and establishing a firm architectural base, perhaps a framework with stronger conventions is a better choice.
You do you
Hopefully, the generalized structure presented in the post gives you a reasonable kernel upon which to graft your personal preferences. There are many ways to deviate from these patterns, and no objectively “correct” architecture.
Maybe you’d prefer to have controllers instantiate views rather than the
App. Maybe the data layer should be isolated behind services. Or maybe the
App and module abstractions should be merged.
I would encourage you to experiment, and find the conventions that work best for you.
Take a breather
That’s a wrap on Part I. In Part II, we will examine a handful of disjoint extensions to the basic organizational patterns posited in Part I. But before we get started, take a break, and prepare your mind and body by perusing this album of posing DJs.