We all know JavaScript does not implement a native methods overload concept and what we usually do on daily basis is to emulate somehow this Classic OOP behavior.
There are several ways to do it, all of them with pros and cons, but what is the best way to implement it?
A common situation
Let's imagine we would like to have a method able to accept different arguments types and return something accordingly with what we received.Usually in Classic OOP an overload cannot redefine the returned type but in JavaScript we can do "whatever we want", trying to be still consistent, so that we can consider us more lucky than C# or Java guys, as more flexible as well.
For this post, the behavior we would like to obtain will be similar to the one we can find in jQuery library, one of the most used, famous, and friendly libraries I know.
// create an instance
var me = new Person();
// set some property
// via chained context
me
.name("Andrea")
.age(31)
;
// retrieve some property
me.name();
// Andrea
While the classic get/set concept is boring, old style, and not optimized, the chainability one is really smart, semantic, easy to understand, and bytes saver.
As example, compare above piece of code with this one, following comments to understand which part is not that convenient:
// create an instance
var me = new Person();
// set some property
me.setName("Andrea");
// unoptimized operation, a setter
// does not usually return anything
// we would expect at least the set parameter
// as if we did
// (me.name = "Andrea")
// we could expect true/false
// as operations flag
// but when do we need a "false"
// able to break our code in any case?
// throws an error if the operation
// was not successful since we cannot
// go on with what we expect
// set something else
me.setAge(31);
// we need to rewrite the var name
// for every single method
// plus we need to write methods with
// everytime a prefix: get or set
// this is redundant and inefficient
me.getName(); // Andrea
If we pass under minifiers and mungers this silly little example, we'll find out that the chain strategy goes from 72 to 56 bytes, while the get/set goes from 76 to 68.
As summary, while overload strategies could be adopted in both cases, the example code will use the first strategy to perform the task: "create a Person class and a method able to behave differently accordingly with received arguments"
Inevitably via "arguments"
As is for every scripting language, the basic strategy to emulate an overload, is the lazy arguments parsing.Lazy, because we need to understand for each method call what should be the expected behavior.
This is surely a performances problem so even if the topic in this case is overload, we should never forget that sometimes the best way to set or get something, is setting or getting something:
// if we can set something
// the internal property is somehow exposed
// in any case since it is reconfigurable
me.setName("Andrea");
me.setName("WebReflection");
me.getName();
// against
me.name = "Andrea";
me.name = "WebReflection";
me.name;
Avoid pointless overheads
Of course getters and setters could be considered safer, but how many times we have spotted code like this?
// somewhere in some prototype
setStuff: function (_stuff) {
this._stuff = _stuff;
},
getStuff: function () {
return this._stuff;
}
Above code simply adds overhead for each operation performed against the _stuff property, and what's the point to do it in that way?
Unless we don't ensure a type check for each call, above code style over a programming language without type hints could be simply considered an error, since every time we spot a badly implemented design, error is usually the first thought we have.
The Simplest Overload Implementation
Latest code could be already compressed into a single method. We want to perform different operations, accordingly with a generic argument, plus we would like to "kick Java Devs ass" overloading the returned value as well to make chainability possible.
function Person(_name) {
// we would like to have a default name
this._name = _name || "anonymous";
}
Person.prototype = {
constructor: Person,
name: function (_name) {
// what should we do?
// if _name is not undefined or null
if (_name != null) {
// we want to set the name
// (eventually doing checks)
this._name = _name;
// and return the Person instance
return this;
} else {
// nothing to set, it must be a get
return this._name;
}
}
};
A simple test case:
var me = new Person();
alert([
me.name(), // anonymous
me // magic chain!
.name("Andrea")
.name() // Andrea
].join("\n"));
Where Is The Overload
When we talk about overload, we think about multiple methods invoked runtime accordingly with received arguments.Since as I have said we cannot do this in JavaScript:
// what we are basically doing
// ECMAScript 4th Representation
function name(_name:String):Person {
this._name = _name;
return this;
}
// overload
function name(void):String {
return this._name;
}
// what Java/C# guys should write
// to emulate the current behavior
class Person {
object name(string _name) {
this._name = _name;
return this;
}
// overload
object name() {
return this._name;
}
}
// example
string name = (string)(
(Person)(
new Person()
).name("Andrea")
).name();
all we usually do is to put a lot of if, else if, else, switch statements inside the same method.
Overload Pros
I do believe it's hardly arguable that overloads are able to make code easier to maintain and both more clear and more linear.While strict typed languages do not need to take care about arguments type, scripting languages are often focused about this matter and the code inside a single method could be messy enough to be hardly maintainable or debuggable.
In few words, with overloads we can split behaviors incrementing focus over single cases delegating arguments parsing, when and if necessary, in the single exposed entry point: the method itself.
Overload Via Polluted Prototype
The first overload implementation to obtain exactly the same behavior is this one:
Person.prototype = {
constructor: Person,
// exposed public method
name: function (_name) {
// exposed entry point
// where we decide which method should be called
return this[_name == null ? "_nameGet" : "_nameSet"](_name);
},
// fake protected methods
_nameSet: function (_name) {
this._name = _name;
return this;
},
_nameGet: function () {
return this._name;
}
};
Pros
We can "cheat" a little bit in the entry point referencing the this variable to decide which "protected" method we need to call.In my case I don't even mind if _nameGet does not accept an argument, which is still a bad design since the operation is just tricky without a valid reason, but at least we have more flexibility. If we think that every method could have common operations inside, e.g. a User class with a protected this._isValidUser() performed via every other method call, this approach could be considered good enough to split all matters/logics we are interested about.
Cons
Specially for debug, all those faked protected methods could be really annoying. All we want to show/expose are public methods so that a for in loop or a console.log operation, as example, will show just the "public status" of an instance and not every single internal method we are not interested about. Moreover, some "clever" developer could decide to use internals calls but we would like to avoid any kind of critical operation, don't we?Overload Via Closure
This second approach is able to maintain overload benefits, exposing only what we want:
Person.prototype = {
constructor: Person,
name: (function(){
// method closure
// we can share functions
// or variables if we need it
// the method, named, easier to debug
function name(_name) {
return _name == null ? get.call(this) : set.call(this, _name);
}
// overload
function set(_name) {
this._name = _name;
return this;
}
// overload
function get() {
return this._name;
}
// the exposed method
return name;
}())
};
Pros
A for in loop will pass only the constructor and the public method so that nobody can change, use, or modify its internals. Even other methods cannot access into "name" closure so we are free to use common meaningful name as is as example for both get and set.
If at some point every overload needs to perform the same task, we can simply create another function so that this will be shared in the whole closure.
Cons
Somebody could argue that those internal functions are not Unit Test friendly ... at least this was my guess before I talked with a skilled programmer that said: "dude, you should test what you expose and not what is the exposed internal logic. In other words, it's the name method the one you care about, and not what happens inside which will be implicitly tested via 'name' calls".Due to the closure, it is not possible to share across the prototype a single function reused for each call.
This is not something truly problematic, since in this case we can always use an outer closure:
var User = (function () {
// private method shared in the whole prototype
function _isValidUser() {
// db operations here
// this is just a silly example
return this._name != null && this._pass != null;
}
// User class
function User() {}
User.prototype = {
constructor: User,
updateAge: function (_age) {
if (!_isValidUser.call(this)) {
throw new Error("Unauthorized User");
}
this._age = _age;
},
verifyCredentials: (function (){
// internal closure
function verifyCredentials(_user, _pass, create) {
if (create === true) {
_create.call(this, _user, _pass);
}
return _isValidUser.call(this);
}
function _create(_user, _pass) {
this._name = _name;
this._pass = _pass;
}
return verifyCredentials;
}())
};
return User;
}());
Optimized Overload via Closure
Since size always matters, and this is valid for performances too, this is an alternative closure example:
Person.prototype = {
constructor: Person,
name: (function(){
function name(_name) {
return _name == null ? get(this) : set(this, _name);
}
function set(self, _name) {
self._name = _name;
return self;
}
function get(self) {
return self._name;
}
return name;
}())
};
Everything else is the same, except we assume that every private/internal method won't require a call/apply, simply the instance as mandatory first argument.
Patterns Size And Performances
All patterns have been passed under YUICompressor with all features enabled. This is the result, in numbers:- Simplest Overload (nothing is splitted, everything inside the single method): before 308, after 162, compression ratio: 47%.
- Polluted Prototype: before 384, after 233, compression ratio: 39%.
- Overload via Closure: before 464, after 240, compression ratio: 48%.
- Optimized Overload via Closure: before 464, after 224, compression ratio: 52%.
Simplest Overload
We can easily deduct that the most common approach to behave differently is the classic method with "everything inside". Under the microscope, performances will be slightly better than every other approach since the number of function calls is reduced. But, we should consider that my example does basically nothing and that in most common cases the whole function body is polluted with large amount of variables, sometimes potentially disturbing (e.g. for (var i, ...)) and we can hardly debug long methods with hundreds of checks and if else there. Of course the example I have chosen does not really represent the perfect scenario where it's evident how much cleaner overloads are against common code style, so in that particular case, I would have chosen the first approach, but this is up to us, and "case per case" dependent.
Polluted Prototype
While performances and semantic could appear a bit better, the compression ratio in this case is the worst one. Moreover, this approach implicitly suffers name clashes problem. We should simply think about chained inheritances, and how many times we could have the same name over different mixins or classes.If we add what we have already understood about Cons, I would define this approach the less convenient one, while it is still probably the most adopted one starting from the classic protected method approach.
Overload via Closure
This approach is already good for its purpose. Not a single compiler can optimize a this reference so I do hope developers will start to get rid of the classic call/apply approach in favor of the self one. THere are no benefits against the polluted prototype, byteswise speaking, but it is still valid all Pros against the latter one.Optimized Overload via Closure
This pattern is the real winner:
function Person(a){this._name=a||"anonymous"}Person.prototype={constructor:Person,name:(function(){function b(d){return d==null?a(this):c(this,d)}function c(e,d){e._name=d;return e}function a(d){return d._name}return b}())};
As we can see, the number of this references inside the overloaded method is reduced to three, rather than N for each private method call. The compression ratio is best one as is the size, except for the basic case.
Lots of pros, and "just a matter of style" as cons, I already like this latest pattern!
No comments:
Post a Comment