In Part I, we established basic organizational patterns for separating Backbone.js applications into layers. In Part II, we will explore various complimentary patterns and refinements to those described in Part I.
Extension 1 – Reuse between modules with mixins
After building a few microapps, you will likely wish to share views and models between modules. The generalization and reusability of views is, in fact, one of the primary motivations for developing the controller abstraction. For instance, you may develop a generic
Reusable behavior should be extracted into a
Core module, which is distinct from other modules. Reusable functionality can take the form of base classes, like a
However, inheritance isn’t our strongest choice. Truly modular, cross-cutting behavior does not comport well with a parent/child relationships.
In the example above, I wouldn’t consider
WordsView to be a subtype or specialization of
ListView, because while
WordsView requires list-like behavior, it is not, within our domain, conceptually a child of
ListView. List-like behavior is a mechanical, incidental convergence of
WordsView, not a hierarchical relationship we benefit from expressing, like how an
AccountManger might be a subclass of
The mixin approach opens up the possibility of extending multiple objects:
Now we have a modal list!
Extension 2 – Store ephemeral state on the controller
Not all application data needs to be persisted to the server. State might also be encoded into the URL, placed in local browser storage, or simply stored as ephemeral state in memory and lost upon refresh.
Scenarios for ephemeral state storage might include:
- Whether a list is currently expanded or collapsed
- The current search query
- Whether sorting is enabled
In a universe of only Backbone views and models, the view would have been the logical place to store ephemeral state. However, once we introduce the controller abstraction, controllers become the more logical facility.
Avoiding ephemeral state storage in our views means that they can remain as generic as possible, and we can maximize reusability.
Commentmodel might have an unpersisted “editing” attribute. When an admin clicks the “edit” link in a threaded list of comments, the “editing” attribute is set to
true. A separate controller and view can listen to the
CommentCollectionfor changes in the “editing” attribute, and display a modal edit form when editing is activated.
Extension 3 – The delegate pattern
Apple’s Cocoa framework uses the delegate pattern to mediate communication between the view layer and the controller layer.
View controllers are typically the delegate or data source objects for many types of framework objects. The delegating object keeps a reference to the other object—the delegate—and at the appropriate time sends a message to it. The message informs the delegate of an event that the delegating object is about to handle or has just handled
What would this look like in Backbone?
Here is an example of familiar events based communication between the controller and the view.
To achieve the delegate pattern, we can invert dependencies such that the view accepts a controller option. The view then calls methods directly on the delegate object.
The primary advantage of the delegate pattern is that it obviates the need to subscribe to events in the controller. We can slim down our controllers and potentially elucidate control flow with explicit method calls on the delegate rather than the relative indirectness of event subscription.
But really, these two patterns are not substantively different. Either the view is calling methods on the controller delegate or
Backbone.Events is calling methods on the controller. Firing an event is just an indirect version of delegation.
Extension 4 – Expanding beyond controllers
The putative goal of the controller abstraction is to absolve our views of domain logic. This does not imply, however, that our migrated domain logic must live in the controller. In fact, vesting the controller with too much domain logic is as perilous as authoring logic heavy views.
It is often said that a controller should know what to do, but not how to do it. The controller is a coordinating entity, a mediator. It delegates labor between application layers. For instance, the view layer might interpret a user action as “the comment resource should be saved”, and the controller delegates the saving procedure to a comment model.
One should be able to read controller source code and get a good sense of how the application is wired at a high level without getting bogged down in procedure.
Carving out new domain objects
Consider the resume ensmartening module from Part I. The module will likely include a
Resume model to store resume attributes such as “text” and “date.”
We could give
ensmarten method, which returns a new and improved
Resume instance, but ensmartening is a sophisticated process, and it transcends the core responsibilities of
Resume: persisting and retrieving data. As such,
Resume is not a good home for
ensmarten. Similarly, the controller, which is not supposed to house procedural code, is also not a good home for
Our best course of action is to build a POJO dedicated to resume improvement. This new object acts as a service which composes lower level
ResumeEnsmartener is invoked from the controller.
It is vital that the controller layer stays relatively thin and delegates work to domain objects with simple, declarative interfaces. Domain objects can take the form of abstract models, services, policy classes, data repositories, etc., and are ideal for coordinating lower level entities like Backbone models.
Extension 5 – Using an app namespace as a messaging bus
On occasion, unrelated module components require a means of communication. One approach to solving this problem is to enable event publishing and subscribing on the module itself.
Analytics is a reasonable use case for this sort of thing. Many heterogeneous controllers might want to publish their activity, and it is the prerogative of a separate service to choose which published controller events to translate into analytics.
For example, in the thesaurus app, the search controller originally emitted analytics events like this:
This approach requires that we pass the analytics service to every controller, and that specific controllers create analytics events when appropriate. To gain high level insight into which analytics we’re tracking, we’d need to dive into each controller and search for
Let’s refactor analytics tracking out of the controller. The first step is to refactor the controller to trigger events on the messaging bus, the
Thesaurus analytics service now translates module events into analytics:
Or, for extra credit, we can simplify the implementation a bit by partially applying
A word of caution
Use the global messaging bus pattern sparingly, and reserve it for events which are truly global in nature within the context of your app. It is possible, but thoroughly inadvisable, to wire the entire application together via global events. Such abuse of the pattern makes it very difficult to discern object dependencies, and dissolves the structure of the app.
Extension 6 – Marionette
This wouldn’t be a Backbone.js blog post without at least one obligatory “go check out Marionette” solicitation. Marionette expands the Backbone universe, and includes a module system, events aggregation, controllers and more.
A Parting, Shameless Plug