Sunday, November 11, 2012

Prototypal inheritance decoration

Javascript is a mixed bag. You can use a functional style or you can do it with OO. When you do OO you can use prototypal inheritance or you can use it with classical inheritance (it still uses prototypes but the pattern is more classical). I usually use Classical inheritance because it is well understood, performs well and because it's embedded in Closure Tools. But like OO and functional styles we can mix the two together, so should we always stick to one type? If you read my last post then you know there are new features coming for PlastronJS, and one of them in particular has started me thinking about using the prototypal inheritance.

mvc.Collection will soon be changed so that it will not have sort order, instead to apply a filter or a sort order you will need to make a new object. However I want that new object to still have the same features as mvc.Collection and in particular if you add a new model to the new object I actually want it to be put on the original collection. I'm basically decorating the mvc.Collection with new functionality (kind of like decorating a function). To do this with classical inheritance I would need to create a class for these new functions, pass in the collection and reproduce each of the functions to call the corresponding function on the collection. This would be nice and easy using the new Proxy object as I could just say if the name doesn't exist on the object then try call it on the collection, but it's not supported everywhere yet. So what I really want to do is put the collection in the prototype chain and here is where the fun begins.

inheriting


let's call the mvc.Collection class A. I create a new collection: a

now I'd like to have an mvc.Filter class B. What I need to do is something like this:

F = function();
F.prototype = a;
f = new F();
B.prototype = f;
B.prototype.collection_ = a;
b = new B();

I could just as easily have done: B.prototype = a; but the issue is then any methods I put on B.prototype might overwrite a's methods. I've also put in collection_ so I have a reference directly to the collection. So how does this differ from the current use of goog.inherits for classical inheritance? Well the biggest difference is that we're seting the prototype to an instantiated object rather than just the prototype of a class which means we now have access to the instance properties.

this

now the tricky bit. "this" will refer to the object that I call. So if I have b then any time I call:

b.method();

then this will point to b. That isn't an issue if you're worried about getting/setting items that are only on b, but what about when we want to access and change items on our original collection? Well we're safe as long as we don't use '='. Here is the problem:

a.a = '1';
a.inc = function(num) {
  this.a = this.a + 1;
};
a.inc(); // b.a == 2 - number is from object 'a';
b.inc(); // b.a == 3 - number is from object 'b';
a.a; // 2

when we called inc we did it with the context being 'b'. Because we used the assignment operator we created a new property on the instance of 'b', effectively hiding the property of 'a' which is what we really wanted to change. We can either redefine any method on B to call itself on the collection (so the collection becomes the context) which may be a pain or we can instead design A to never use the assignment operator (after the constructor).

No More Assignments

There does have to be some assignments obviously, but you should try to restrict these to the constructor, or on scoped variables (variables without a preceding "this"). But how can that be done? What if I wanted to do something like this:

A = function() {
  this.country = 'USA';
}

a.setCountry = function(country) {
  this.country = country;
}

a.getCountry = function(country) {
  return this.country;
}

The problem here is that country is a simple type that can not be changed. If we do anything to a string the system creates a new object in memory and points to that. We want the 'this.something' to point to the same bit of memory and there are two types that work like that: objects and arrays. So we have two options:

A = function() {
  this.country = ['USA'];
}

a.setCountry = function(country) {
  this.country[0] = country;
}

a.getCountry = function(country) {
  return this.country[0];
}

// or

A = function() {
  this.country = {val: country};
}

a.setCountry = function(country) {
  this.country.val = country;
}

a.getCountry = function(country) {
  return this.country.val;
}

You may think that you could get away with wrapping any simple mutable properties you're putting on an object like this:

A = function() {
  this.properties = {
    prop1: 'a',
    prop2: 'b'
  }
};

but this makes it difficult if you have a chain of objects each with it's own property object. Instead of just getting b.property[0] you will have to recurse through all the prototypes looking in each 'properties' object until you find the one you need, rather than just letting javascript go up the chain for you.

This looks like a fair bit of effort, and it is, but hopefully you won't have too many mutable properties on an object. There is another step of complication though, what about objects and arrays.

object and array setting

In mvc.Collection it currently will make an unsafe copy of the attributes so they can be compared. This means we have something like:

this.prev_ = goog.unsafeClone(this.attr_);

which you can see is an assignment. However there is a lot of setting of the attr_ object and I wouldn't like to put it all under another array or object. If you're using an array or object then you can instead set them by clearing all the keys/values and copying over the keys/values from the other object. This has the unfortunate side affect that your new set object will not satisfy a comparison with the original object.

Calling the super


calling methods on the super should be pretty easy actually, just put in this.collection_.mehod() and you're done

And that's about it - the real trick is knowing what 'this' pertains to.

No comments:

Post a Comment