This recipe is entirely geared toward long term maintainability of Backbone applications.
As Backbone applications grow, they can quickly pollute the global browser namespace with classes, instances and helper variables. In smaller implementations, this may not be much of a concern. When applications grow (and shouldn’t they grow?), a poorly chosen naming scheme can cause all manner of problems.
Two different approaches have worked for us in the past.
The first involves placing all Backbone class definitions inside of a global "namespace" object:
var Calendar = {
Models: {},
Collections: {},
Views: {}
};Then, as you define each model, collection and view class (as well as any helper classes that you might require), you can define them in this structure:
Calendar.Models.Appointment = Backbone.Model.extend({
// ...
});
Calendar.Models.Holiday = Backbone.Model.extend({
// ...
});
Calendar.Collections.Appointments = Backbone.Collection.extend({
// ...
});The principal advantage of this approach is that only one variable representing Backbone models, views and collections makes its way into the global namespace: Calendar. All other classes are defined inside this global object.
This is especially handy when concepts like "holidays" might have meaning outside of the Backbone application. With this naming scheme, the Holiday model is tucked away inside the Calendar.Models "namespace" where intent is clear.
Using this naming convention has the side-benefit of making class naming easier. If you attempt to define things in the global namespace, it might make sense to name the appointment model Appointment. But what, then, should you name the appointment view?
Tacking on the word View to each seems, er… tacky. Models would be given first-class citizen treatment (e.g. Appointment). Collections might get this as well (e.g. Appointments). But views need the extra View? Ugh.
And what if some views do not conflict with models and collection names (e.g. AddAppointment)? Do you append "View" to these in order to follow a convention? Do you omit "View" on these special cases to aid clarity?
Following the global object namespace convention means not having to choose. You get a clear convention that aids in maintainability with the added benefit of limiting risk of conflicting class and variable names.
The global object namespace is a simple approach. It also lends itself well to very large applications with numerous classes that you might prefer to keep in separate files. For more self-contained applications, a full blown Javascript object makes more sense.
This approach involves using a function constructor [11]. With a function constructor in Javascript, all manner of objects, functions, data and even classes can be initialized inside the function. Very little need be exposed to the outside world.
In the following, methods and data are defined inside the public and private object literals. With that out of the way, object initialization can be performed. Lastly, public attributes—and only the public attributes—of the resulting object are returned:
var Calendar = function(options) {
var private = { /* private stuff here */ },
public = { /* stuff for the outside */ };
// Define methods and properties, adding to public as needed
// Perform any object initialization
return public;
}
var calendar = new Calendar();In the global object namespace approach, we would still need to assign collection and view variables in the global namespace:
var appointments = new Calendar.Collections.Appointments;
With a function constructor, this instance creation can take place inside of the function where there is no danger of conflicting with other variables.
Applying this approach to a Backbone application, we have something along the lines of:
var Calendar = function() {
var Models = { /* model classes */ };
var Collections = { /* collection classes */ };
var Views = { /* view classes */ };
// Initialize the app
var appointments = new Collections.Appointments;
new Views.Application({collection: appointments});
return {
Models: Models,
Collections: Collections,
Views: Views,
appointments: appointments
};
};By defining Models, Collections, and Views object literal variables inside of the constructor, we have an easy time referencing things across—or even outside—concerns. For example, when initializing the Appointments collection, we can refer to new Collections.Appointment. With the global namespace approach, we always have to use the global namespace new Calendar.Collections.appointment. Dropping a single word does wonders for code readability and long term maintainability.
By returning each of these classes from the function constructor with a key of the same name, it means that the outside can get access to objects or classes with the exact same naming scheme:
var Calendar = function() {
// ...
var appointments = new Collections.Appointments;
return {
Models: Models,
Collections: Collections,
Views: Views,
appointments: appointments
};
};
var calendar = new Calendar();
// The appointments collection class
var AppointmentsCollection = calendar.Collections.Appointments;
// Actual appointments from the initialized collection object
var appointments = calendar.appointments;Encapsulating concepts inside functions is so useful, in fact, that it can be used inside the function constructor. Instead of assigning the Models, Collections, and Views variables directly to object literals containing class definitions, we find it best to assign them to the return value of anonymous functions. These anonymous functions, when invoked with the () operator, return the same object literals—but with only those attributes that we want exposed to the outside world.
For example, instead of defining the Models variable directly:
var Calendar = function() {
var Models = {Appointment: Backbone.Model.extend({...});};
// ...
};We can define the Models inside an anonymous function:
var Calendar = function() {
var Models = (function() {
var Appointment = Backbone.Model.extend({...});
return {Appointment: Appointment};
})();
// ...
};In this case, it buys us nothing. The end result of both approaches is a Models object variable with an Appointments key. This Appointments key references the Appointment class. This allows us to reference the class as Models.Appointment from elsewhere inside the function constructor (and ultimately as Calendar.Models.Appointment outside of the function constructor).
Where this approach yields benefit is when you have classes that are only needed by other classes, but not the outside world. For instance, the Appointment model may need to create instances of appointment attendees:
var Calendar = function() {
var Models = (function() {
var Appointment = Backbone.Model.extend({
// ...
attendees: function() {
_.(this.get("emails")).map(function(email) {
return new Attendee(email);
});
}
});
var Attendee = Backbone.Model.extend({ /* ... */ });
// Only return Appointment
return {Appointment: Appointment};
})();
// ...
};This is especially powerful with View classes. Generally, only a handful of View classes need to be seen outside of a Backbone application. The remaining only pop-up on demand from the main view.
In the following, only the Application view needs to be accessed from outside. Once it is initialized, instances of the remaining classes are used on demand from the Application object or from each other:
var Calendar = function() {
var Models = (function() { /* ... */ })();
var Collections = (function() { /* ... */ })();
var Views = (function() {
var Appointment = Backbone.View.extend({...});
var AppointmentEdit = Backbone.View.extend({...});
var AppointmentAdd = new (Backbone.View.extend({...}));
var Day = Backbone.View.extend({...});
var Application = Backbone.View.extend({...});
return {Application: Application};
})();
// Initialize the app
var appointments = new Collections.Appointments;
new Views.Application({collection: appointments});
return {
Models: Models,
Collections: Collections,
Views: Views,
appointments: appointments
};
};A second advantage of this approach is that, within the constructor, it is possible to reference cross concern classes with less ceremony. When the collection needs to reference the model, it can do so as Models.Appointment instead of the full Calendar.Models.Appointment that is required in strategy #1:
var Collections = (function() {
var Appointments = Backbone.Collection.extend({
model: Models.Appointment,
parse: function(response) {
return _(response.rows).map(function(row) { return row.doc ;});
}
});
return {Appointments: Appointments};
})();This simple, and seemingly small, change will pay significant dividends over the lifetime of your Backbone applications.
This advantage is even more pronounced when referencing classes within the same concern. For example, if clicking the day view spawns the add-appointment view, this can be done with a simple reference to AppointmentAdd instead of needing to type (and read) Calendar.Views.AppointmentAdd:
var Day = Backbone.View.extend({
events : {
'click': 'addClick'
},
addClick: function(e) {
console.log("addClick");
AppointmentAdd.reset({startDate: this.el.id});
}
});The last advantage of this approach is the ability to define a very specific API for your Backbone application. Only those properties and methods required by other objects or even other Backbone applications are exposed.
A potential disadvantage of this approach is that individual model, view and collection classes cannot be in separate files and included directly in the page:
<script src="/javascript/backbone/calendar/models/appointment.js"> <script src="/javascript/backbone/calendar/collections/appointment.js"> <script src="/javascript/backbone/calendar/views/appointment.js"> <script src="/javascript/backbone/calendar/views/appointment_edit.js"> <script src="/javascript/backbone/calendar/views/appointment_add.js"> <script src="/javascript/backbone/calendar/views/day.js"> <script src="/javascript/backbone/calendar/views/application.js">
In the end, the choice is yours. Stick with the simple, global object that allows separate files for each class or go for the self-contained goodness of javascript objects. One is sure to meet your needs.
Namespacing is one of those concepts that you generally do not think about until it is too late. Even if you are fairly certain that your Backbone application is going to remain small, it is best to initialize and build models, views and controllers inside a common namespace object. This eliminates questions about possible naming conventions and reduces the footprint on the global namespace.
But, if your application is large or has the potential to grow large, it is best to put Javascript’s function constructors to good use. These can create a whole application constructor that only exposes those pieces that you definitely want the rest of the page to see. Better still, it gives the individual components of your application more direct access to each other.
[11] This approach relies on Javascript closures. If this is a foreign concept, check out "Inheritance: Functional" in Douglas Crockford’s "Javascript: The Good Parts"