Skip to content
This repository

An argument for `_configure` #1462

Closed
ianstormtaylor opened this Issue June 27, 2012 · 18 comments

6 participants

Ian Storm Taylor Jeremy Ashkenas brad dunbar Derick Bailey Paul Uithol Tim Branyen
Ian Storm Taylor

I don't think I was communicating this well, so I tried to write up my thoughts on it and hopefully it is convincing. Would love to know what you guys think.


For the most part, you can augment Backbone however you want easily and everything is peachy. But changing the initial Backbone configuration logic is not one of those cases.

It's common practice to make "base" classes when using Backbone so that your customatizations apply to all of your app's classes. Instead of extending from Backbone.Model, you always extend from BaseModel.

That works amazingly for customization on a per-class level. What I mean by that is that if you have an AuthorModel, BookModel, and PageModel, they will all need different initialization logic. All you need to do is extend from BaseModel and then write a custom initialize function in each of those models.

It also works well for customization on a class-wide level for anything post-configuration. By that I mean that despite AuthorModel and BookModel being significantly different, they might both make use of a cleanAttributes method you've defined in BaseModel.

Where the whole thing breaks down is when you want to do customize the configuration on class-wide level. There's nothing to hook into to augment the configuration logic of anything that extends from BaseModel before initialize is called. Say I want to change the way BaseModel works a bit by copying the options that get passed in onto this.options to match how Views treat options. Or maybe I want to add a way for BaseModel to store extra UI state in a this.state property instead of inside attributes. Or maybe I want to have my models inherit defaults all the way up the chain by _.defaulting them.

Another way to think about it is that there are four main places you might want to hook into to add custom logic in to a Backbone class. The first two are handled fine as is, but there isn't a nice way to handle the third:

Before anything is configured: Here you could provide your own augmented constructor. (But this could be done by augmenting _configure instead, which I also think is preferable.)

After it is initialized: This is completely up to you, and most of what you're writing as a Backbone user fits in this category.

After things are configured, but before initialize: Backbone doesn't give you a nice way to do this! There are only two annoying ways and one incomplete way I could think to do it:

Augment the constructor. (incomplete)

constructor : function (attributes, options) {
    this._configureOptions();
    Backbone.Model.prototype.constructor.apply(this, arguments);
}

That does work, but only if your added logic doesn't rely on anything else Backbone does in its constructor. For example, if you need to access this.attributes which have been parsed and attached directly to the instance, you're out of luck. Or if need to reference the cid? tough.

Re-call your logic in every single initialize method. (annoying)

var AuthorModel = BaseModel.extend({

    initialize : function (attributes, options) {
        this._configureOptions();
        // Real initialization logic.
    }
});

var BookModel = BaseModel.extend({

    initialize : function (attributes, options) {
        this._configureOptions();
        // Real initialization logic.
    }
});

var PageModel = BaseModel.extend({

    initialize : function (attributes, options) {
        this._configureOptions();
        // Real initialization logic.
    }
});

Yeah I could do that. But I shouldn't need to keep copying and pasting that code around all over my codebase when I know upfront that I want everything that extends BaseModel to be configured that way.

Give up initialize and create a new initialize. (annoying)

initialize : function (attributes, options) {
    this._configureOptions();
    this.start.apply(this, arguments);
}

start : function (attributes, options) {
    // Real initialization logic.
}

That's also not great. Everyone who reads my code needs to be informed that what they have always known as initialize is now start. Any time I'm talking/reading about Backbone I have to mentally convert initialize to start and back. And if you accidentally override initialize instead, you might not even realize it until things get weird because the instance wasn't configurated properly.


So how should it be done? My suggestion is to make all the Backbone classes have a _configure method that does the configuration that happens before initialize in the constructor. With a _configure method the problem is easily solved and you end up with this:

_configure : function (attributes, options) {
    Backbone.Model.prototype._configure.apply(this, arguments);
    // Add custom logic here.
}

You just augment _configure in your BaseModel and then you don't ever need to touch it again! All of your other extended Models will get the augmented configuration options. Class-wide configuration solved!

Look familiar? That's because Backbone.View already has a _configure method that makes this kind of augmentation incredibly easy. But the other Backbone classes should get _configure too.

And the _configure solution doesn't have any effects on the rest of Backbone source.

Bonus: it's great for mixins too!

The best way I've found to add functionality to an existing Backbone class without extending a completely new class is the mixin pattern. It's like the jQuery plugins of Backbone. Having a _configure would make it extremely easy for mixins to be grafted onto existing classes (regardless of whether it's a View, Model, Collection or Router!).

// Augmented `_configure` to call `_inherit`
var _configure = this.prototype._configure;
this.prototype._configure = function () {
  this._inherit(this.inherits || []);
  _configure.apply(this, arguments);
};

Since the classes all have the _configure method, you can attach your custom logic to each of them the same way in your mixin. And then when you package your mixin for others, they can add inheritance to any class they want with the same syntax:

inheritMixin.call(BaseView);
inheritMixin.call(DropdownModel);
inheritMixin.call(AppRouter);
Ian Storm Taylor

You can also see #1446 for a code example.

Ian Storm Taylor

@braddunbar @jashkenas Can you guys tell me why this is bad at least?

brad dunbar
Collaborator

Good morning @ianstormtaylor, thanks for writing up your thoughts on this. I mostly agree about the pain points of writing plugins and doing initialization. However, I disagree with the proposed solution.

Your problem, as I understand it, is that plugins and other Backbone objects meant to be inherited from need a way to specify logic to be executed before initialize is called in their child classes. Usually, the place for this is initialize but since you'd like to keep it clean for inheriting classes (calling the super method is not required) this presents a problem. So, inserting a method (_configure) in between the logic in the constructor and calling initialize is the proposed solution.

In my opinion, there is already a perfectly good way to accomplish this, which is to require child classes to call the parent implementation when providing initialize. This is a well understood technique and requires no extra explanation or code. This situation has come about in the first place because Backbone attempted to circumvent calling super methods with initialize. I don't think that adding more methods will simplify it. In fact I think it will make things worse.

In short, I don't think that avoiding calling super methods because of verbosity is a good enough reason to add complexity in the form of more configuration methods. In fact, I would advocate going the other direction entirely and removing _configure from Backbone.View.

Derick Bailey

The view's _configure method exists because it has to parse out the model, collection, and other options that you can specify when instantiating a view. Maybe this needs to be renamed, but not removed.

I'm also not a fan of requiring extending classes to call super methods for something like initialize. This method is so basic and so fundamental to any object that extends from a Backbone construct. It should never be implemented by a base type - a type that is built with the explicit intent of never being instantiated directly, but always extended from.

Other methods... well... if you want to override the delegateEvents method, then that's your choice and you'll have to call the super method yourself.

The distinction in my mind is methods that exist purely for the extending classes to implement (initialize, render, etc) vs methods that are core to the functionality of Backbone constructor, delegateEvents, etc.

All that being said, though, I don't understand the need to run code after constructor but before initialize the way this ticket suggests. Can you provide a more specific, real world example of how one of your projects really needs this, @ianstormtaylor ?

In all my work with Backbone, building plugins, creating project specific abstractions, etc, I've never seen a need to do anything other than provide a constructor method on a base type.

brad dunbar
Collaborator

The view's _configure method exists because it has to parse out the model, collection, and other options that you can specify when instantiating a view. Maybe this needs to be renamed, but not removed.

Sorry if I was unclear, I meant that the logic included in _configure could be moved into the constructor, not removed entirely. In any case, _configure is an implementation detail and should not be relied upon in extensions.

I'm also not a fan of requiring extending classes to call super methods for something like initialize. This method is so basic and so fundamental to any object that extends from a Backbone construct. It should never be implemented by a base type - a type that is built with the explicit intent of never being instantiated directly, but always extended from.

The problem is that, with multiple levels of inheritance, you'll never get around this issue. Once some class has implemented initialize, all classes that extend from it must call the parent implementation. I suppose that's fine if you only anticipate one level of inheritance, but you certainly can't guarantee that.

Derick Bailey

The problem is that, with multiple levels of inheritance, you'll never get around this issue. Once some class has implemented initialize, all classes that extend from it must call the parent implementation. I suppose that's fine if you only anticipate one level of inheritance, but you certainly can't guarantee that.

I don't think it has anything to do with multiple levels of inheritance. i have multiple levels of inheritance in Backbone.Marionette's view framework: Marionette.View -> Marionette.ItemView -> Marionette.Layout. None of these base types implements the initialize method. I leave that up to the person extending from ItemView or Layout.

These are base types - never meant to be instantiated directly, always meant to be extended from. Base types should provide the needed functionality without requiring a call in to the super method. We'll never get away from super method calls, 100% - but we can leave core methods like initialize alone in our base types, and not require users to call super for them.

But again, that's for base types. If I have an AddressEditForm in my project, I'm probably going to have an initialize method in this type. If someone decides to extend from it, they will have to call the super type's initialize if they need to implement this method.

Paul Uithol

What I'm doing in Backbone-relational for initialization is a different variant, which boils down to:

    Backbone.RelationalModel = Backbone.Model.extend({
        _isInitialized: false,

        set: function( key, value, options ) {
            Backbone.Model.prototype.set.apply( this, arguments );

            if ( !this._isInitialized ) {
                // Call whatever initialization logic you need
                this._isInitialized = true;
            }

            return this;
        }
    });

This works because set is called right before initialize in the constructor (or at least close enough). It's a bit better than the other variants because you won't override set very often, and if you do, you'll normally want to call the parent implementation regardless. Having a proper, built-in solution would still be preferable though.

Ian Storm Taylor

The way I arrived at the _configure solution is because if you just ask yourself "where should this extra configuration logic go for my BaseView?", the answer is pretty simply: "right after Backbone does it's normal configuration logic, AKA right before initialize". A lot of the cases could probably be covered by adding the logic before Backbone's own logic by overriding constructor, but that bars anyone from augmenting internal Backbone logic for no reason.

For example, if View did change to remove the _configure method, and I decided that I wanted to remove this.options because it just encourages people accessing things directly instead of through the given getter/setters (which we've agreed upon before), I would have to do that in every initialize across my entire app. That's ridiculous.

Yes it does work, but it's janky because I already know I want to apply to all my classes, but there's no way for me to do that so I have to do it in initialize all the time instead.

@PaulUithol Yeah that technically works, but it's just a hack because you don't have the hook you need. You shouldn't need to store initialize state and augment set to get functionality. You could also do the same in Collection by grafting your initialization logic on _reset as well, but that would also just be a hack. And then what's worse is if you're writing this as a mixin, you need to add a bunch of different logic for each potential class that it could be mixed in to. If it's View, override _configure; Model, override set and store initialized state; Collection, override _reset and store initialized state; Router, override _bindRoutes. When instead all of them could override a _configure method.

@derickbailey I think the problem is that a lot of the times, having the logic at the beginning or at the end of the constructor works, but if it is after it means none of the views that inherit from it can opt-out because it hasn't even happened by the time initialize is called. And if it's before, like I mentioned you can't override Backbone core logic.

@braddunbar Why should _configure not be used by mixins? What's wrong with having a pre-defined place for configuration to be and allowing people to augment it to support their specific use case? The way I see it, Backbone always claims it doesn't want to support edge cases in core, which is awesome, but then why make augmenting like this so hard. I think constantly calling configuration logic in initialize or calling up the chain to BaseView regardless of how many children deep your are is just a janky workaround that only has to happen because there's no good place to add configuration logic.

There's a big difference between logic a specific type of View needs to have in initialize to set up it's specific needs, versus logic you know you want to have app-wide from the start in all your Views. For Views (right now) you're fine because overriding _configure is easy. But more Models and Collections there's no good place to add that logic.

brad dunbar
Collaborator

Why should _configure not be used by mixins?

The leading underscore and lack of documentation denote that _configure is an internal method and can be changed or removed at any point.

I think constantly calling configuration logic in initialize or calling up the chain to BaseView regardless of how many children deep your are is just a janky workaround that only has to happen because there's no good place to add configuration logic.

I disagree. Calling the super method is a well understood method for extension in javascript, not a workaround. Whether or not it should be the preferred technique is this case is a different question.

Derick Bailey

@ianstormtaylor can you provide a specific example of where you need this, to make your application behave the way you need it to behave?

Having a specific example, where the functionality you need for your application's behavior is best facilitated in this way, may help to make your case much stronger and may help others to see what your needs are. It's possible that someone might suggest an alternative that suits your needs. And it's possible that seeing a specific example would convince the core contributors to accept this pull request.

Tim Branyen
Collaborator

For what it's worth I use _configure inside LayoutManager. Without it, the ability to bind the reference to render inside initialize like below:

this.model.on("change", this.render, this);

would not be possible.

Ian Storm Taylor

Here's one case where I have a mixin that adds state handling to any Backbone class: Backbone State and the important part inlined:

(function (_, Backbone) {
  Backbone.mixin || (Backbone.mixin = {});
  Backbone.mixin.state = function () {

    // Augmented `_configure` to call `_configureStates`.
    var _configure = this.prototype._configure;
    if (this.prototype._previousAttributes || this.prototype._prepareModel) {
      this.prototype._configure = function (arg, options) {
        _configure.apply(this, arguments);
        this._configureStates(this.states || [], options);
      };
    } else {
      this.prototype._configure = function (options) {
        _configure.apply(this, arguments);
        this._configureStates(this.states || [], options);
      };
    }

    // Setup `this.state` to store state values, and grab apply any initial state values passed in as options.
    this.prototype._configureStates = function (states, options) {
      this.state = {};
      for (var i = 0, state; state = states[i]; i++) {
        this.state[state] = options[state] === true;
      }
    };
    Backbone.mixin.state.call(BaseView);

Then I never have to worry about setting up states again, and the states are setup by the time initialize is called in a Model, Collection or View.

Ian Storm Taylor

I also think @tbranyen's is a good example of how the View's configure method is useful.

Derick Bailey

For what it's worth I use _configure inside LayoutManager. Without it, the ability to bind the reference to render inside initialize ... would not be possible.

I wouldn't say "not possible". I'm doing it with Marionette, without using _configure: https://github.com/derickbailey/backbone.marionette/blob/master/src/backbone.marionette.itemview.js#L8-20

...

I'm starting to see the value of something like what this ticket is suggesting. Personally, I think _configure is the wrong method name, though. "_methods" shouldn't be messed with, IMO. They are meant to be private.

But something along these lines could be of great benefit to the plugin developers like @PaulUithol, @tbranyen and myself, and to applications in general where project-specific abstractions and layers are being created.

Tim Branyen
Collaborator

@derickbailey Its not possible without creating a new constructor, that's not how LayoutManager works.

var CollectionView = Backbone.View.extend({
  initialize: function() {
    this.collection.on("reset", this.render, this);
  }
});

Is the functionality this provides.

Ian Storm Taylor

I'd gladly have it be configure instead of _configure, I was just going for parity with View for now.

Edit: updated the pull request to have public configure.

Ian Storm Taylor

Another example of a useful mixin that would be able to be class-agnostic if they had a common configure hook: https://groups.google.com/forum/?fromgroups=#!topic/backbonejs/NIxDt5NsoN4 Just came up in the Backbone Google Group

Jeremy Ashkenas
Owner

I'm not sure that I really follow what this ticket is asking for. It seems like it's mostly a request to add additional empty methods to the initialization step so that you can add code in various subclasses without ever having to call super.

To which my response would be ... just call super.

That said, if it's something you want to add to your base model yourself, it should be pretty easy:

BaseModel = Backbone.Model.extend({

  configure: function(){},

  constructor: function() {
    this.configure();
    Backbone.Model.apply(this, arguments);
  },

})
Jeremy Ashkenas jashkenas closed this December 04, 2012
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.