bendrucker / ama

Ask me questions about building web applications
MIT License
6 stars 1 forks source link

Bookshelf prototype vs. class properties #11

Closed demisx closed 9 years ago

demisx commented 9 years ago

Hi Ben, When creating a new bookshelf model, we have an option of adding properties either to the model prototype or to the constructor function as class properties, i.e.:

bookshelf.Model.extend([protoProps], [classProperties]) 

Do you have a rule of thumb to share, when mode properties should be added as proto or as class? My understanding, that prototype properties are shared between all model instances, but without access to private vars and class properties are created for each object separately and have access to private vars. I can't seem to make up a rule in my head, so it's easy to remember.

Thank you.

bendrucker commented 9 years ago

I wouldn't think in terms of a rule of thumb here. What's more important is whether you can let JS help you via the implicit this. Think of this as a special variable, i.e. fn.apply(specialArg, regularArgs). The only magic is that evaluating obj.fn() will implicitly call fn on obj.

When you're assigning something to the prototype, it is indeed shared by all instances. Individual instances can also overwrite that property for just themselves without affecting other instances. I would add the concept of "private" to the mix since the way you're describing isn't really correct anyway.

An easy example of a case where you want a prototype method is validation. Given Model.prototype.validate, the validate function will receive the model instance it was called from (i.e. model.validate() as this. Then you can use the model properties in validate.

A comparable class method would be:

Model.validate = function (model) {
  doValidation(model.attributes);
};

In that case you're wasting code. By using the implicit this you save the trouble of passing model around.

Class methods (static in ES6 parlance) tend to be best reserved for methods that don't use instance data. If you wanted to implement an Active Record interface with methods like create and find, you'd do that with static methods. Bookshelf generally favors the approach of defining instances and then it uses the attributes of the instance to generate queries, as in new Model({id: 1}).fetch() instead of Model.fetchById(1). There are a lot of static helpers though that do the later by using the former under the hood.

The other thing to keep in mind about static methods is that you may still have a use case for this:

class Model {
  static where (params) {
    return query(params);
  }
  static all () {
    return this.where();
  }
}

class User extends Model {
  static where (params) {
    params = params || {};
    params.active = true;
    return super.where(params);
  }
}

In that example, User defines a static method where that overrides Model.where. But because Model.all uses the dynamic this instead of explicitly referencing Model.all, our inheritance/override pattern is more powerful. User.all will only get active users.

demisx commented 9 years ago

Excellent answer! Coming from a Java class-based inheritance to protypical-inheritance does make some things confusing to me cause I guess I expect JS to work sometimes as Java would. It's getting better though. :) Thank you very much for your time explaining in such detail. It really helps.

P.S. Bummer I have to close this, since many others could find this answer useful too. Maybe it could be a blog post at some point?

bendrucker commented 9 years ago

It can! Too little time, too many post ideas. Got a few flights coming up. Might try to bang out one each.