22

I'm using Backbone.js for the first time, and liking it so far. One thing I can't work out at the moment in dynamic attributes of models. For example, say I have a Person model, and I want to get their full name:

var Person = Backbone.Model.extend({
  getFullName: function () {
    return this.get('firstName') + ' ' + this.get('surname');
  }
});

Then I could do person.getFullName(). But I'd like to keep it consistent with the other getters, more like person.get('fullName'). I don't see how to do that without messily overriding Person#get. Or is that my only option?

This is what I've got so far for the overriding option:

var Person = Backbone.Model.extend({
  get: function (attr) {
    switch (attr) {
    case 'fullName':
      return this.get('firstName') + ' ' + this.get('surname');
      break;
    case 'somethingElse':
      return this.doSomethingClever();
      break;
    default:
      return Backbone.Model.prototype.get.call(this, attr);
    }
  }
});

I suppose it's not terrible, but it seems there should be a better way.

Skilldrick
  • 69,215
  • 34
  • 177
  • 229

3 Answers3

51

Would this be simpler?

var Person = Backbone.Model.extend({
  get: function (attr) {
    if (typeof this[attr] == 'function')
    {
      return this[attr]();
    }

    return Backbone.Model.prototype.get.call(this, attr);
  }
});

This way you could also override existing attributes with functions. What do you think?

Skilldrick
  • 69,215
  • 34
  • 177
  • 229
bytespider
  • 586
  • 5
  • 6
  • Yeah - that does work, good idea. My only worries are (a) it's a tiny bit magic, and (b) that I'd have to put all the dynamic attributes as top-level methods of the model. But maybe that's what I want anyway! I think I'm going to stick with the switch case for now, but I may come back to this... Cheers. – Skilldrick Jul 14 '11 at 16:32
  • I take it back - this is much better than the switch case approach, mainly because it's much more conducive to longer accessor methods. – Skilldrick Jul 14 '11 at 16:51
  • I love this. Beautiful solution! – David Tuite Dec 07 '11 at 04:19
  • 3
    This is a great answer. I took this idea forward and created a getter/setter plugin (https://github.com/berzniz/backbone.getters.setters) – Tal Bereznitskey Feb 23 '12 at 20:43
  • This is great as a solution, but I don't see how I could do both getters AND setters, @TalBereznitskey, since they would both call this.[attr]. Wish I could though! – SimplGy Feb 20 '13 at 02:16
  • @Simple - some of the jqery functions use the same function to set and get - haven't looked at the code but presumably they count the parameters – ErichBSchulz May 05 '13 at 02:22
  • 1
    @Simple, got intrigued, presumably doing an `if` on `arguments.length==0` see http://stackoverflow.com/questions/2141520/javascript-variable-number-of-arguments-to-function – ErichBSchulz May 05 '13 at 02:50
  • I am inclined to disagree with this answer, while it is seamless, you run into other problems like displaying properties in the view, which would cause confusing errors to others who don't know about your special get method. You could rectify that by overriding other methods on backbone, but that is starting to get out of hand if you ask me. – Eric Wooley Aug 21 '14 at 18:35
2

I would think of attributes as the raw materials used by a model to provide answers to callers that ask the questions. I actually don't like having callers know too much about the internal attribute structure. Its an implementation detail. What if this structure changes?

So my answer would be: don't do it.

Create a method as you've done and hide the implementation details. Its much cleaner code and survives implementation changes.

Bill Eisenhauer
  • 6,183
  • 2
  • 30
  • 28
  • 5
    The point is that the caller shouldn't have to know whether fullName is an attribute or a method - all properties of the object should have the same interface, in this case `.get(attr)`. That seems to be the best way to hide implementation details, IMHO. – Skilldrick Jul 14 '11 at 16:36
  • You leak implementation details by requiring the caller to know internal attribute names. Yes, you could say that that is the interface, but I prefer methods which require no parameters and which never would need to change. Personal preference, I guess. – Bill Eisenhauer Jul 14 '11 at 16:42
  • 2
    Yes, but the whole way Backbone works is by using the attribute names. I don't want to have to define getters and setters for every attribute in my models when I have a getter and setter already. I do see your point, but I think it goes against the way Backbone was set up to work. – Skilldrick Jul 14 '11 at 16:52
  • Yes, it goes against what Backbone provides you, but you are assuming that you have to use this alone to define your interface. A more pure OO approach would define only the getters and setters that need to be surfaced to callers. I view it as lazy to rely on dangerous framework methods as one's interface. But to each his own, sounds like you have a reasonable answer for you. – Bill Eisenhauer Jul 14 '11 at 19:53
  • Either way, the person using the interface needs to know the model's attribute names. You can let him know by setting them in the `defaults` object or by creating getters and setters. Both valid, but sticking to the standard `.get(attr)` would be considered better API design: anyone who knows the core library, knows how to use your API. – cabe56 Apr 24 '13 at 05:40
1

The actual properties used by Model.get are stored in the attribute property. You could do something like this:

// function to cross-browser add a property to an object
function addProperty(object, label, getter, setter) {
    if (object.defineProperty){
      object.defineProperty(object, label, {getter: getter, setter: setter})
    }
    else {
        object.__defineGetter__(label, getter)
        object.__defineSetter__(label, setter)
    }
}

// inside the initializer of your model, add a property to the attribute object
var Person = Backbone.Model.extend({
    initialize: function(attr, options) {
        var t = this;
        ...
        addProperty(this.attributes, 'fullName',
            function() {return t.get('firstName') + ' ' + t.get('surname'),     
            function(val) {...}
        )
    }  
})

This will allow you to do person.get('fullName') as you requested.

Edit: To be clear, I agree with Bill's answer below. Shouldn't really be dinking around with the internal implementation of backbone.js. Especially since this is incomplete...what about escape() instead of get()? And the setter is more complex, as it does validation, change notification, etc...now I'm sorry I posted this :)

Skilldrick
  • 69,215
  • 34
  • 177
  • 229
John
  • 171
  • 1
  • 5
  • Here's another option. Instead of trying to override the behavior of get(), why don't you provide new semantics. Extend Model with something like 'myGetter()' and 'mySetter()'. Always call these custom methods, never call get() directly. Within myGetter(), either redirect to your custom property or call the default get() as appropriate. The benefit of this is that you'll have consistent semantics wherever you call from, and you will be resilient to backbone.js implementation changes. – John Jul 15 '11 at 00:20