-
-
Notifications
You must be signed in to change notification settings - Fork 5.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Backbone Model defaults system is not subclass safe #476
Comments
The same problem is present for the |
The defaults property can be used as a function, https://github.com/documentcloud/backbone/blob/master/backbone.js#L136. Backbone.View's events don't support that, and maybe support should be added, but for now you could call |
Yep, if you'd like to use |
Rather than making everyone use functions for their defaults, can I propose a different solution (and hopefully get this ticket re-opened)? Backbone uses its special: var extend = function (protoProps, classProps) { That's not production-ready code, but hopefully you get the idea. If this were implemented, everyone could sub-class to their heart's content AND get to use objects for their defaults ... var A = Backbone.Model.extend({defaults:{foo:"bar"}}); |
@machineghost |
Well if you want to do something fancy, clearly functions are the way to go. But it seems to me that 80% of the time (complete BS figure) when someone wants default inheritance they want simple inheritance, ie. "keep the values that come from my parent (or my parent's parent, or my ...) unless I define a different value". I'm assuming this because it's how both OOP and prototypal inheritance works. If you delete all values from a parent class because a sub-class dares to define a single value (even if it's a completely different value from any set by the parent), you're doing something very unfamiliar to anyone who is familiar with inheritance. As for "this functionality would also change other classes" ... we're talking about five lines here right? Five lines that I imagine don't change very often. Making a new modelExtend copy of the existing function would add neither significant weight nor would it be a maintainability problem (if you change extend once a year, you have to change the same line twice, once a year; and does extend even change once a year?). So, I hear what you're saying; you've got this great simple extension mechanism, and it will be less simple if you change it in the way I propose. But given that it's not getting problematically less simple, and given that it would give users the kind of inheritance they expect from an inheritance framework, isn't it worth it? |
Just to try and show why this matters, here's defaults inheritance done using the two different approaches: // My proposed form // Current form Now imagine you're a new Backbone user: If the only issue is keeping the special extend function "DRY" (don't repeat yourself), and not forking it in to two functions ... is that really worth causing all of the above confusion? |
This is exactly what's happening. The value of |
So, that's totally one way to think of it; defaults are all one property. From an implementation standpoint, that's exactly the right way to think about it, because the defaults are literally a single JS property. But, that ignores how Backbone actually works/is used: you don't do fooModel.get(), you do fooModel.get("bar"). In other words, attributes isn't just "one property"; Backbone encourages you to think (and rightly so) that each attribute is a property of the model. foo.get("bar") != foo.get("baz"), even though foo.attributes == foo.attributes. So, I'd argue that because Backbone doesn't work directly with .attributes, and because it does have get/set/a bunch of other methods that treat each attribute as a property, the defaults of the attributes should be thought of the same way: not as a single defaults property, but as a fooDefault, a barDefault, etc. And when you think of it that way, it makes absolutely no sense why setting a fooDefault would have any effect whatsoever on your barDefault. |
I might have a related problem. Once I changed my I defined
I then extended that base class and instantiated several instances of the extended model and added properties to the
The problem is that both I'm guessing that a deep copy of the defaults object is not being done. |
@tauren You're right, no copying of defaults is being done so any non-primitive value you set will be used for all instances of that type. Using a function creates a new defaults object on every call and sidesteps the issue. |
@braddunbar Yep, and in fact the docs do mention this. I need to re-read the docs, as the last time I went through them was quite a while ago.
|
This is the biggest pitfall I see from developers switching from a classical inheritance language into prototypical inheritance (JavaScript). In the case of Backbone, because you actually call it defaults` it really should be getting cloned every time you extend a Model, View, etc. This is very simple to do, in the
|
The shared reference issue is totally a valid one, but why stop with cloning when Backbone can just as easily provide actual inheritance on defaults? Remember, clone is nothing but a wrapper around extend, so instead of the base clone extend:
Backbone could do:
and then a the default for Foo on class A wouldn't be erased by setting a default on Bar on class B (that subclasses A). |
Braddunbar, that issue was closed with a documentation-only change. I'm trying to advocate for an actual fix (which would also be a fix for the events issue because they both use the same behavior of replacing rather than extending). |
@machineghost Backbone is already providing a prototypal inheritance scheme. |
Not all prototypal inheritance systems are equal ;-) And that's the thing: this isn't about prototypal inheritance vs. classical, it's about two different views of how to implement prototypal inheritance in Backbone. What it basically comes down to is that you can look at defaults and events as individual properties, but if you're going to do that then why not look at attributes as an individual property too? When someone does set("foo", "bar"), behind the scenes let's do attributes = {"foo": "bar"). Now that would (obviously) be silly, because "attributes" isn't a property, it's a collection of properties. Similarly, events and defaults aren't just individual properties, and I really think continuing to pretend that they are is only going to cause more problems and make the framework weaker overall. As soon as you start treating them the same way you treat attributes, suddenly this issue goes away. And likewise, the issue with objects in defaults goes away. (Plus, I'd think most use-cases for function-based defaults would go away too). Implementation-wise none of this is hard; just use _.extend and call it a day. As for backwards-compatability:
There you go; now every new user gets proper inheritance on events and defaults that works the same as the rest of Backbone (ie. attributes) does, and doesn't require warnings like "Remember that in JavaScript, objects are passed by reference, so if you include an object as a default value, it will be shared among all instances." (warnings which are easily missed and confuse new users). Existing Backbone users will likely welcome these changes (but don't take my word for it: you could always make a poll asking people if they want simple inheritance or extend-based inheritance). If some don't, or if they just can't afford to fix issues that might crop up they just have to add a single line (Backbone.useSimpleInheritance()), which they can add at the same time they increment the version number in the file name (because none of this affects them until they upgrade). Heck, maybe some people will even continue using this option for performance reasons in the future ... but most will probably think extend-based inheritance is so natural, they'll quickly forget defaults/events ever did anything differently. So basically, everyone wins with this approach. |
Just got bit by this!! Seems unexpected behavior from conventional OO inheritance model, where subclasses inherit not only methods but properties (attributes) of parent class. I'd like to see this feature added -- or a non-clunky alternative provided -- perhaps a 'defaults2.0' :), '_defaults', 'inheritableDefaults' , etc. if backward compatibility is the issue. |
@moos the simple answer is if you want inheritance, you should build it yourself. see: https://github.com/documentcloud/backbone/issues/search?q=events+extend You can also take the approach that Chaplin takes and collect instances up the prototype chain yourself: chaplinjs/chaplin#295 and utils.coffee#L60-67 |
I don't think you should build inheritance in JS yourself. Just avoid it. Use composition. |
Backbone provides a standard 'defaults' object literal that you can add to a Model to establish defaults. However, if you are subclassing models then this is not safe.
For example, imagine if you have:
var Car = Backbone.Model.extend({
defaults: {
property1: 'foobar'
}
});
var Toyota = Car.extend({
defaults: {
property2: 'hello'
}
});
When Toyota is made property2 will be present, but not property1.
Backbone should instead have a getDefaults() method that provides an object literal. Subclasses can over-ride this (and call their super-classes getDefaults() to chain defaults together) so that when Backbone mixes its defaults in it can aggregate them all together, with something like:
Backbone.Model = function(attributes, options) {
attributes || (attributes = {});
attributes = _.extend({}, this.getDefaults(), attributes);
Where the default getDefaults() implementation would just be an empty {}.
The text was updated successfully, but these errors were encountered: