My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.

Friday, November 25, 2011

On Complex Getters And Setters

A common use case for getters and setters is via scalar values rather than complex data.
Well, this is just a programmer mind limit since data we could set, or get, can be of course much more complex: here an example

function Person() {}
Person.prototype.toString = function () {
return this._name + " is " + this._age;
};
// the magic identity configuration object
Object.defineProperty(Person.prototype, "identity", {
set: function (identity) {
// do something meaningful
this._name = identity.name;
this._age = identity.age;
// store identity for the getter
this._identity = identity;
},
get: function () {
return this._identity;
}
});

With above pattern we can automagically update a Person instance name and age through a single identity assignment.

var me = new Person;
me.identity = {
name: "WebReflection",
age: 33
};

alert(me); // WebReflection is 33

While the example may not make much sense, the concept behind could be extended to any sort of property of any sort of class/instance/object.

What's Wrong

The problem is that the setter does something in order to keep the object updated in all its parts but the getter does nothing different than returning just the identity reference.
As summary, the problem is with the getter and the reason is simple: from user/coder perspective this may not make sense!

me.identity.name = "Andrea";

alert(me); // WebReflection is 33

In few words we are changing an object property, the one used as identity for the me variable, leaving the instance of Person untouched ... and semantically speaking this looks wrong!

A Better Approach

If the setter aim is to update a state there will be only a well known amount of properties to re-assign before this status can be updated.
In this example we would like to be sure that as soon as the identity has been set, the instance toString method will produce the expected result and without relying an external reference, the identity object itself.

// listOfNames and listOfAges are external Arrays
// with same length
for (var
identity = {},
population = [],
i = 0,
length = listOfNames.length;
i < length; ++i
) {
// reuse a single object to define identities
identity.name = listOfNames[i];
identity.age = listOfAges[i];
population[i] = new Person;
population[i].identity = identity;
// out of this loop we want that each
// instanceof Person prints out
// the right name and surname
}

// we cannot do it lazily in the toString method ...
// this will fail indeed
Person.prototype.toString = function () {
return this._identity.name + " is " + this.identity._age;
};

Got it? It's basically what happens when we use an object to define a property, more properties, or to create inheriting from another one: properties and values are parsed and assigned at that moment, not after!

var commonDefinition = {
enumerable: true,
writable: true,
configurable: false
};

var
name = (commonDefinition.value = "a"),
a = Object.defineProperty({}, "name", commonDefinition),
name = (commonDefinition.value = "b"),
b = Object.defineProperty({}, "name", commonDefinition)
;
alert([
a.name, // "a"
b.name // "b"
]);

As we can see if the property assignment would have been lazy the result of the latest alert would have been "b", "b" since the object used to define these properties has been recycled ... I hope you are still following ...

A Costy Solution

There is one approach we may consider in order to make this identity consistent ... the one that stores an identity with getters and setters.

// the magic identity configuration object
Object.defineProperty(Person.prototype, "identity", {
set: function (identity) {
var self = this;

// do something meaningful
self._name = identity.name;
self._age = identity.age;

// store identity as fresh new object with bound behavior
this._identity = Object.defineProperties({}, {
name: {
get: function () {
return self._name;
},
set: function (name) {
self._name = name;
}
},
age: {
get: function () {
return self._age;
},
set: function (age) {
self._age = age;
}
}
});
},
get: function () {
return this._identity;
}
});

What changed ? The fact that now we are able to pass through the instance and operate through the identity:


me.identity.name = "Andrea";

alert(me); // Andrea is 33

Cool hu ? ... well, it could be better ...

A Faster Solution

If for each Person and each identity we have to create a fresh new object plus at least 4 functions, 2 for getters and 2 for relative setters, our memory will fill up so quickly that we won't be able to define any other person identity soon and here comes the fun part: use the knowledge gained by this pattern internally!
Yes, what we could do to make things slightly better is to recycle the internal identity setter object definition in order to borrow maximum 4 functions rather than 4 extra per each Person instance ... sounds cool. uh?

function Person() {}
Person.prototype.toString = function () {
return this._name + " is " + this._age;
};

(function () {

var identityProperties = {
name: {
get: function () {
return this.reference._name;
},
set: function (name) {
this.reference._name = name;
},
configurable: true
},
age: {
get: function () {
return this.reference._age;
},
set: function (age) {
this.reference._age = age;
},
configurable: true
},
reference: {
value: null,
writable: true,
configurable: true
}
};

Object.defineProperty(Person.prototype, "identity", {
get: function () {
return this._identity;
},
set: function (identity) {
// something meaningful
this._name = identity.name;
this._age = identity.age;

// set the reference to the recycled object
identityProperties.reference.value = this;

// define the _identity property
if (!this._identity) {
Object.defineProperty(
this, "_identity", {value: {}}
);
}
Object.defineProperties(
this._identity,
identityProperties
);
}
});

}());

var
identity = {},
a = new Person,
b = new Person
;

identity.name = "a";
identity.age = 30;
a.identity = identity;

identity.name = "b";
identity.age = 31;
b.identity = identity;

alert([
a, // "a is 30"
b // "b is 31"
]);

So what we have there? A lazy _identity definition, good for those scenario where some getter or setter may never been invoked, plus a smart recycled property definition through a single object descriptor, and performances boosted up N times per each instance since no multiple different functions are assigned per identity properties getters and setters and no extra objects are created runtime ... arf, arf .. are you still with me ?

As Summary

Some JS developer keep asking for standard ways to do these kind of crazy stuff without realizing that few other programming languages are that flexible as JavaScript is ... it's maybe not that simple to find better, optimized, in therms of both memory consumption and raw performances, patterns to cover weird scenario but what we should appreciate is that with JS we almost have, always, a way to simulate something we did not even think about until the day before.
Have fun with JS ;)

No comments: