Skip to content

Instantly share code, notes, and snippets.

@aron
Last active January 4, 2016 00:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save aron/ca16c1e614fdf1399d3b to your computer and use it in GitHub Desktop.
Save aron/ca16c1e614fdf1399d3b to your computer and use it in GitHub Desktop.
Proposal for using real constructors throughout the analytics codebase.

Using named constructors

The general usage of a Backbone class is to use the .extend method providing an .initialize method.

var MyView = Backbone.View.extend({
  initialize: function () {
    // Setup object.
  }
});

In the background this returning a throw-away constructor object to create the new instance, and then calling .initialize to handle setup. The up-side of this, and the only reason I can see for it's existence, is that we don't have to call the parent Backbone.View method with our new instance.

The downside to this is that it removes the benefits of having a named function as the constructor. Namely debugging and introspection.

var MyView = Backbone.View.extend({});
MyView.name //=> ""
new MyView().constructor.name //=> ""
 
function MyOtherView() {
  Backbone.View.apply(this, arguments);
}
MyOtherView.name //=> "MyOtherView"
new MyOtherView().constructor.name //=> "MyOtherView"

Using the Chrome console to inspect objects is a common task, and without named constructors it can be confusing as to which type of object you are looking at.

Take the following example:

var Car = Backbone.Model.extend({initialize: function () {}});
var Bus = Backbone.Model.extend({initialize: function () {}});
var Van = Backbone.Model.extend({initialize: function () {}});
var VehicleCollection = Backbone.Collection.extend({initialize: function () {}});
var GarageView = Backbone.View.extend({initialize: function () {}});

var vehicles = new VehicleCollection();
vehicles.add(new Car());
vehicles.add(new Car());
vehicles.add(new Bus());
vehicles.add(new Van());

var garageView = new GarageView({collection: vehicles});

Backbone allows us to pass our own constructor into .extend and can be used in place of .initialize and is closer to a bare JavaScript inheritance style. This gives us much clearer output in the Chrome console.

var Car = Backbone.Model.extend({constructor: function Car() {
  Backbone.Model.apply(this, arguments);
}});
var Bus = Backbone.Model.extend({constructor: function Bus() {
  Backbone.Model.apply(this, arguments);
}});
var Van = Backbone.Model.extend({constructor: function Van(){
  Backbone.Model.apply(this, arguments);
}});
var VehicleCollection = Backbone.Collection.extend({constructor: function VehicleCollection() {
  Backbone.Collection.apply(this, arguments);
}});
var GarageView = Backbone.View.extend({constructor: function GarageView() {
  Backbone.View.apply(this, arguments);
}});

var vehicles = new VehicleCollection();
vehicles.add(new Car());
vehicles.add(new Car());
vehicles.add(new Bus());
vehicles.add(new Van());

var garageView = new GarageView({collection: vehicles});

This also allows us to provide more debugging information when passed the incorrect data. For example if we require a specific model type to be passed into a view we can use the name property to provide useful feedback to the developer.

constructor: function MyView(options) {
  Backbone.View.apply(this, arguments);

  if (!(options.model instanceof AwesomeModel)) {
    var modelName = options.model.name || "unknown";
    throw new Error("MyView expected model option to be an AwesomeModel instance but received a " + modelName + " instance");
  }
}

// Later
new MyView({model: SuperModel});
//=> error: "MyView expected model option to be an AwesomeModel instance but received a SuperModel instance"

So, I suggest that we phase out the use of .initialize and instead pass constructors. The functionality is the same, and all it requires is naming the constructor function and calling the parent constructor.

Benefits are clearer code, the inheritance pattern is visible and consistent with extending other methods, and better visual output in browsers consoles that support it.

This is something that can be introduced incrementally without disrupting the existing codebase. But will improve readability as it's introduced.

@grahamscott
Copy link

Hi @aron

Thanks for the detailed write up of the problem and proposed solution. I think it looks great. I don't think it's a big deal to do the small amount of boilerplate wireup in the constructor function, and the debugging benefits on their own would be really useful.

This gets my vote 👍

@johnathan-sewell
Copy link

Nice write up and a clever technique! I think this will make debugging this area much easier.

I was discussing the use of initialize with Ali and I think we just have to be careful to apply the default constructor (Backbone.View.apply(this, arguments);) before we add our own code.

I agree that while we could use initialize and constructor, it might be confusing so we should drop initialize in favour of constructor.

I can't see this being a problem for unit testing, can you?

Looking forward to using it, thanks.

@aron
Copy link
Author

aron commented Jan 22, 2014

@JonathanSewell, yeah I agree about the default constructor, but we use inheritance and over-ridding of methods in many places across the codebase and we seem to be remembering okay so far.

Also agree with dropping .initialize it makes no sense to me to use both. This isn't to say that a constructor use smaller setup functions. Using the constructor also gives you nice encapsulation, you don't have a redundant initialize method hanging around on every instance that could theoretically be called multiple times.

Unit testing is another good point. I used to use explicitly test .initialize to ensure that the state of my instance was setup correctly. But I've since been of the opinion that by thoroughly testing the behaviour of the public facing methods this should be covered implicitly. We seem to be moving in that direction here too, with a focus on private helper functions and exposing only a minimum of methods, so I think this approach compliments this nicely.

@mroderick
Copy link

If I understand things correctly, this will also mean that spying on our Backbone based objects will be less clunky.

//using initialize
var spy = sinon.spy(MyView.prototype, 'initialize');

// using constructor
var spy = sinon.spy(MyView);

I like this much better than using initialize.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment